基于IO/NIO/Netty的Client/Server的java程序实现

一、IO实现

(一)IO介绍

1.传送IO特点

  • (1)服务端阻塞点
    server.accept();获取套接字的时候
    inputStream.read(bytes);输入流读取数据的时候

  • (2)传统socket是短连接,可以做短连接服务器,他无法做长连接,属于一问一答的模式,比如老的tomcat底层用的就是socket,用完就会关掉线程,因此不会出现线程一直被占用的情况,支持处理多个客户端连接

    • 单线程情况下只能有一个客户端(一个线程维护一个连接,也就是一个socket客户连接)线程一直被占用。
    • 用线程池可以有多个客户端连接,但是非常消耗性能(用此案城池,就是老tomcat原理,只不过是用完后就释放)

2.原理图

在这里插入图片描述

(二)IDEA创建Java项目

创建两个项目,分别每个项目新建一个类,分别是client和server

在这里插入图片描述

在这里插入图片描述

(三)代码

1.server服务端



import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class server {
    public static void main(String[] args) throws IOException {
        //创建客户端的Socket对象(SevereSocket)
        //ServerSocket (int port)创建绑定到指定端口的服务器套接字
        ServerSocket ss=new ServerSocket(50000);

        //Socket accept()侦听要连接到此套接字并接受他
        Socket s=ss.accept();

        //获取输入流,读数据,并把数据显示在控制台
        InputStream is=s.getInputStream();
        byte[] bys=new byte[1024];
        int len=is.read(bys);
        String data=new String(bys,0,len);
        System.out.println("数据是:"+data);

        //释放资源
        s.close();
        ss.close();
    }

}



2.client客户端

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class client {
    public static void main(String[] args) throws IOException{
        //创建客户端的Socket对象
        //Socket (InetAddress adress,int port)创建流套接字并将其连接到指定IP地址的指定端口号
//		Socket s=new Socket(InetAddress.getByName("192.168.224.1"), 10000);
        //Socket (String host,int port)创建流套接字并将其连接到指定主机的指定端口号
        Socket s=new Socket("127.0.0.1", 50000);

        //获取输出流,写数据
        //OutputStream getOutputStream();返回此套接字的输出流
        OutputStream os=s.getOutputStream();
        os.write("helloWorld!".getBytes());

        //释放资源
        s.close();

    }

}

(四)运行结果

分别运行两个项目结果如下
在这里插入图片描述

二、NIO实现

(一)NIO介绍

1.NIO的特点

主要API介绍:
ServerSocketChannel对应传统IO中的ServerSocket。
SocketChannel对应传统IO中的Socket。
Selector 是NIO核心 ,负载监听 ServerSocketChannel与SocketChannel ,支持单线程连多个客户端;类似通道管理器而且底层是c实现的;线程拥有一个selector就可以支持多个客户端。
SelectionKey 相当于map中的key 相当于记录根据不同动作做不同事情,一个key一个事件。

2.NIO的通信步骤

①创建ServerSocketChannel,为其配置非阻塞模式。

②绑定监听,配置TCP参数,录入backlog大小等。

③创建一个独立的IO线程,用于轮询多路复用器Selector。

④创建Selector,将之前创建的ServerSocketChannel注册到Selector上,并设置监听标识位SelectionKey.OP_ACCEPT。

⑤启动IO线程,在循环体中执行Selector.select()方法,轮询就绪的通道。

⑥当轮询到处于就绪状态的通道时,需要进行操作位判断,如果是ACCEPT状态,说明是新的客户端接入,则调用accept方法接收新的客户端。

⑦设置新接入客户端的一些参数,如非阻塞,并将其继续注册到Selector上,设置监听标识位等。

⑧如果轮询的通道标识位是READ,则进行读取,构造Buffer对象等。

⑨更细节的问题还有数据没发送完成继续发送的问题…

3.原理图

在这里插入图片描述

4.IO/NIO对比

NIO模型中线程数量大大降低,线程切换效率因此也大幅度提高

  • IO读写以字节为单位
  • NIO解决这个问题的方式是数据读写不再以字节为单位,而是以字节块为单位。IO模型中,每次都是从操作系统底层一个字节一个字节地读取数据,而NIO维护一个缓冲区,每次可以从这个缓冲区里面读取一块的数据, 这就好比一盘美味的豆子放在你面前,你用筷子一个个夹(每次一个),肯定不如要勺子挖着吃(每次一批)效率来得高。
    在这里插入图片描述

(二)IDEA新建JAVA项目

新建两个java项目,每个项目里新建一个类,如下
在这里插入图片描述

(三)代码

1.服务器client:


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.util.Iterator;
import java.util.Set;

public class server {
    //网络通信IO操作,TCP协议,针对面向流的监听套接字的可选择通道(一般用于服务端)
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;

    /*
     *开启服务端
     */
    public void start(Integer port) throws Exception {
        serverSocketChannel = ServerSocketChannel.open();
        selector = Selector.open();
        //绑定监听端口
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        //设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //注册到Selector上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        startListener();
    }
    private void startListener() throws Exception {
        while (true) {
            // 如果客户端有请求select的方法返回值将不为零
            if (selector.select(1000) == 0) {
                System.out.println("当前没有任务!!!");
                continue;
            }
            // 如果有事件集合中就存在对应通道的key
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            // 遍历所有的key找到其中事件类型为Accept的key
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable())
                    handleConnection();
                if (key.isReadable())
                    handleMsg(key);
                iterator.remove();
            }
        }
    }
    /**
     * 处理建立连接
     */
    private void handleConnection() throws Exception {
        SocketChannel socketChannel = serverSocketChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
    }
    /*
     * 接收信息
     */
    private void handleMsg(SelectionKey key) throws Exception {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer attachment = (ByteBuffer) key.attachment();
        channel.read(attachment);
        System.out.println("当前信息: " + new String(attachment.array()));
    }

    public static void main(String[] args) throws Exception {
        server myServer = new server();
        myServer.start(8881);
    }
}

server:

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class client {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);

        // 连接服务器
        if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 8881))) {
            while (!socketChannel.finishConnect()) {
                System.out.println("connecting...");
            }
        }
        //发送数据
        String str = "hello,物联网2019级WOOZI";
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        socketChannel.write(byteBuffer);
        System.in.read();
    }

}

2.客户端server

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class client {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);

        // 连接服务器
        if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 8881))) {
            while (!socketChannel.finishConnect()) {
                System.out.println("connecting...");
            }
        }
        //发送数据
        String str = "hello,一只特立独行的猪";
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        socketChannel.write(byteBuffer);
        System.in.read();
    }

}

(四)运行结果

在这里插入图片描述

三、Netty

(一)Netty介绍

1.简介

Netty封装了JDK的NIO,Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。

2.特点

  • 并发高
    Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。

  • 传输快
    Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。我们知道,Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点,如果数据量大,就会造成不必要的资源浪费。
    Netty针对这种情况,使用了NIO中的另一大特性——零拷贝,当他需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。

  • 封装好

3.Netty通信的步骤

①创建两个NIO线程组,一个专门用于网络事件处理(接受客户端的连接),另一个则进行网络通信的读写。

②创建一个ServerBootstrap对象,配置Netty的一系列参数,例如接受传出数据的缓存大小等。

③创建一个用于实际处理数据的类ChannelInitializer,进行初始化的准备工作,比如设置接受传出数据的字符集、格式以及实际处理数据的接口。

④绑定端口,执行同步阻塞方法等待服务器端启动即可。

4.框架组成

在这里插入图片描述

(二)项目环境配置

1.FIle->Project Structure
在这里插入图片描述
2.MoudlesDependencies→右边的加号
在这里插入图片描述
3.Library->From Maven...
在这里插入图片描述
4.下载netty的jar包
搜索输入io.netty:netty-all,等几分钟

在这里插入图片描述
(2)选择第一个下载
在这里插入图片描述
(3)弹出一个框选择ok
在这里插入图片描述

(4)下载完成,点击OK
在这里插入图片描述

(三)代码

1.Server服务器端
(1)Server,java

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class Server {
    public static void main(String[] args) throws Exception {
        //用于处理服务器端接受客户端连接的线程组
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        //用于进行网络通讯(读写)的线程组
        NioEventLoopGroup workGroup = new NioEventLoopGroup();

        //创建辅助工具类,用于服务器通道的一系列的配置
        ServerBootstrap sb = new ServerBootstrap();
        sb.group(bossGroup,workGroup)//绑定两个线程组
                .channel(NioServerSocketChannel.class)//指定NIO的网络传输模式为TCP,UDP:NioDatagramChannel
                .option(ChannelOption.SO_BACKLOG,1024)//设置tcp缓冲
                .option(ChannelOption.SO_SNDBUF,32*1024)//设置发送缓冲大小
                .option(ChannelOption.SO_RCVBUF,32*1024)//设置接收缓冲大小
                .option(ChannelOption.SO_KEEPALIVE,true)//保持连接
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new ServerHandler());//这里配置具体数据接收方法的处理
                    }
                });

        ChannelFuture cf1 = sb.bind(8787).sync();//异步的绑定指定的端口
        ChannelFuture cf2 = sb.bind(8686).sync();//netty可以绑定多个端口

        cf1.channel().closeFuture().sync();//等待关闭,相当于Thread.sleep(Integer.MAX_VALUE)
        cf2.channel().closeFuture().sync();

        //关闭线程组
        bossGroup.shutdownGracefully();
        workGroup.shutdownGracefully();
    }
}

(2) ServerHandler.java

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;

public class ServerHandler extends ChannelHandlerAdapter {
    /**
     * 重写读数据时处理的方法
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf buf = (ByteBuf) msg;

        //声明字节数组,buf.readableBytes()返回的是buf缓冲中可读的字节数
        byte[] req = new byte[buf.readableBytes()];
        //将buf缓冲区中的字节读取到字节数组req中
        buf.readBytes(req);
        String body = new String(req, "utf-8");
        System.out.println("Server打印接收到的信息:" + body);
        String response = "Server返回给Client的响应信息:" + body;

        //1.ctx.writeAndFlush()方法相当于连续调用了write()和flush()方法,因为write()方法只是将buf写到了渠道的缓冲区中,flush()方法会将缓冲区中的数据传给客户端
        //2.这里Unpooled工具类的作用就是讲字节数组转成netty的ByteBuf对象
        //3.这里使用了writeAndFlush()方法会自动释放buf缓冲区所以不需要想ClientHandler中那样finally中手动释放buf缓冲区了
        //4.addListener()方法:当监听到服务器将数据写给客户端,并且确认客户端已经收到信息后,
        // 服务器端就会主动去关闭跟客户端的连接,因为客户端调用了cf1.channel().closeFuture().sync()方法,所以客户端这里的阻塞就会打开,继续向后执行代码
        ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
//                .addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 重写读数据出现异常处理的方法
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

}

2.client客户端
(1)Client.java

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class Client {
    public static void main(String[] args) throws Exception{

        NioEventLoopGroup group = new NioEventLoopGroup();//用于处理网络通信(读写)的线程组

        Bootstrap b = new Bootstrap();//创建客户端辅助类工具
        b.group(group)//绑定线程组
                .channel(NioSocketChannel.class)//设置通信渠道为TCP协议
                .handler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new ClientHandler());//这里配置具体数据接收方法的处理
                    }
                });

        /*8787端口通讯*/
        ChannelFuture cf1 = b.connect("127.0.0.1", 8787).sync();//异步建立连接

        cf1.channel().write(Unpooled.copiedBuffer("hello world".getBytes()));//将“hello world”写到buf缓冲区
        cf1.channel().flush();//这里必须使用flush(),只用冲刷才能将buf缓冲区中的数据传给服务器端

        /*8686端口通讯*/
        ChannelFuture cf2 = b.connect("127.0.0.1", 8686).sync();
        cf2.channel().writeAndFlush(Unpooled.copiedBuffer("hello netty".getBytes()));

        cf1.channel().closeFuture().sync();//等待关闭,相当于Thread.sleep(Integer.MAX_VALUE)
        cf2.channel().closeFuture().sync();

        group.shutdownGracefully();//关闭线程组
    }
}

(2)ClientHandler.java

import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;

public class ClientHandler extends ChannelHandlerAdapter {
    /**
     * 重写读数据时处理的方法
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            ByteBuf buf = (ByteBuf) msg;

            //声明字节数组,buf.readableBytes()返回的是buf缓冲中可读的字节数
            byte[] req = new byte[buf.readableBytes()];
            buf.readBytes(req);
            String body = new String(req, "utf-8");
            System.out.println("Client打印接收到的信息:" + body);

        }finally {
            ReferenceCountUtil.release(msg);//buf缓冲区使用完了,必须释放
        }

    }

    /**
     * 重写读数据出现异常处理的方法
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

(四)运行结果

在这里插入图片描述

四、总结

本次实验了解了IO/NIO/Netty三者的原理特点,三种编程方式的特点,性能方面,NIO优于IO,Netty优于NIO,相比JDK原生NIO,Netty提供了相对十分简单易用的API,非常适合网络编程。Netty是完全基于NIO实现的,所以Netty是异步的。通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。

五、参考资料

参考博客
Netty在IDEA中搭建HelloWorld服务端并对Netty执行流程与重要组件进行介绍
基于IO、NIO、Netty的Java网络程序(TCP的CS模式的聊天程序)
IO、NIO和Netty
Netty入门(一)——传统IO与NIO比较(一)
Netty入门(二)——传统IO与NIO比较(二)

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值