java编写网络IO


前言:
本文主要使用java编写各种网络IO服务端,并且深入到内核方法的调用

1. BIO

  有如下服务端代码

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

public class SocketIO {

    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(9090);
        System.out.println("step1:new ServerSocket(9090)");

        while (true) {
            Socket client = server.accept(); // Jvm转换成对内核系统调用  阻塞1
            System.out.println("step2:client\t" + client.getPort());

            new Thread(new Runnable() { // Java new Thread 其实是调用内核clone()方法分配操作系统内核线程
                @Override
                public void run() {
                    InputStream in = null;
                    try {
                        in = client.getInputStream();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                        while (true) {
                            String dataline = reader.readLine(); // 真正调用时 阻塞2
                            if (null != dataline) {
                                System.out.println(dataline);
                            }else {
                                client.close();
                                break;
                            }
                        }
                        System.out.println("客户端断开");
                    }catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

        }
    }
}

  它是如果建立tcp连接,并等待客户端调用呢。

我们将该代码放在linux系统上进行编译,执行如下命令

strace -ff -o out java SocketIO

在这里插入图片描述

  输出:step1:new ServerSocket(9090)

查看调用内核日志

在这里插入图片描述

执行如下命令,查看主线程调用情况

vi out.6225

  翻到最后几行

可以查看到如下几行日志

// 调用内核socket得到文件描述符7
socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 7 
// 绑定文件描述符7 到端口9090
bind(7, {sa_family=AF_INET6, sin6_port=htons(9090), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::", &sin6_addr), sin6_scope_id=0}, 28) = 0
// 监听文件描述符7    
listen(7, 50) 
// 阻塞1 等待客户端连接 相当于java调用accept()
accept(7   

另启一页,连接服务端,执行命令如下:

nc localhost 9090

服务端输出如下:

在这里插入图片描述

再次执行如下命令,查看主线程调用情况

vi out.6225

在这里插入图片描述

// 与客户端建立socket连接 8是唯一文件描述符
accept(7, {sa_family=AF_INET6, sin6_port=htons(55302), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_scope_id=0}, [28]) = 8

// new Thread()  JVM调用内核的clone方法,分配操作系统内核线程 线程id 6260
clone(child_stack=0x7f2f75f8cfb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f2f75f8d9d0, tls=0x7f2f75f8d700, child_tidptr=0x7f2f75f8d9d0) = 6260
   
// 阻塞1 等待新客户端连接 相当于java调用accept()
accept(7

我们执行vi out.6260 查看new thread生成的线程做了什么。

翻到最后一行

// 阻塞2  接受客户端发送的数据
recvfrom(8
  • 从上面可以看出,整个过程有2个阻塞,一个是服务端阻塞等待客户端连接,连接成功后,新线程阻塞等待已经连接的客户端发送数据,而主线程继续阻塞等待新客户端连接。
  • 问题点:如果有1w个客户端连接服务端,那么服务端会抛出1w个线程,这么多线程资源就是一笔不小的开销;再者如果这1w个线程都是活跃线程,那么线程上下文切换也是很大的性能消耗;还有java线程依赖内核线程进入阻塞状态,也是一笔开销。

这里我们可能会思考几个问题

  1. 内核调用 accept 方法生成的 socket 是什么?
      accept 函数返回的新 socket 其实指代的是本次创建的连接(在linux,用文件描述符表示),而一个连接是包括两部分信息的,一个是源IP和源端口,另一个是宿IP和宿端口。所以,accept 函数可以产生多个不同的 socket,而这些 socket 里包含的宿IP和宿端口是不变的,变化的只是源IP和源端口。

  2. 操作系统如何知道发送网络数据对应哪个 socket 连接?
      socket 连接包含有宿IP、端口和源IP、端口,Clietn 发送的数据包携带这两部分数据,数据包含有的宿IP、端口,可以让数据准确的到达 Server;而 Server 的网卡中断程序,解析数据包中的源IP、端口,就知道这个数据包应该发给哪个 socket 连接了。

2. NIO

  有如下服务端代码

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

public class SocketNIO {

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

        LinkedList<SocketChannel> clients = new LinkedList<>();

        ServerSocketChannel ss = ServerSocketChannel.open();
        ss.bind(new InetSocketAddress(8090));
        ss.configureBlocking(false); // 重点 os NONBLOCKING!!!

        while (true) {
            Thread.sleep(1000);
            SocketChannel client = ss.accept(); // 不会阻塞

            if (client == null) {
//                System.out.println("null......");
            }else {
                client.configureBlocking(false);
                int port = client.socket().getPort();
                System.out.println("client...port:" + port);
                clients.add(client);
            }

            ByteBuffer buffer = ByteBuffer.allocateDirect(4096); // 可以在堆里  堆外

            for (SocketChannel c : clients) { // 串行化!!!  多线程!!
                int num = c.read(buffer); // 不会阻塞
                if (num > 0) {
                    buffer.flip();
                    byte[] aaa = new byte[buffer.limit()];
                    buffer.get(aaa);

                    String b = new String(aaa);
                    System.out.println(c.socket().getPort() + " : " + b);
                    buffer.clear();
                }
            }
        }

    }

}

我们将该代码放在linux系统上进行编译,执行如下命令

strace -ff -o out java SocketNIO

在这里插入图片描述

有这些线程启动

在这里插入图片描述

执行如下命令,查看主线程调用情况

tail -f out.6247

在这里插入图片描述

  • 内核accept方法是非阻塞的,返回-1表示不阻塞,没有拿到客户端连接,被主线程循环调用

另启一页,执行如下命令调用服务端

nc localhost 8090

查看服务端主线程日志

在这里插入图片描述

端口为43884的客户端连接进来,文件描述符9是这次socket连接的唯一标识

在这里插入图片描述

主线程循环等待新客户端连接进来(accept方法非阻塞),循环访问连接进来的客户端是否接受到数据(read方法非阻塞)

  • 从上面可以看出,等待客户端连接的内核方法accept是非阻塞,读取客户端数据内核方法read也是非阻塞。
  • 问题点:如果有1w个客户端连接服务端,如果只有一个客户端发送数据过来,还是得去循环访问每个客户端是否发送数据过来,这样是很浪费性能的。

3. 多路复用 select、poll

  在linux系统下,执行如下命令,查看多路复用器介绍

man select
#include <sys/select.h>
		// nfds: 多少个文件描述符,readfds:文件描述符集合......
       int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                   fd_set *exceptfds, const struct timespec *timeout,
                   const sigset_t *sigmask);
// 描述
DESCRIPTION
       select()  and pselect() allow a program to monitor multiple file descriptors, waiting until one or
       more of the file descriptors become "ready" for some class of I/O operation......

在这里插入图片描述

  • 通过一次系统调用,把fd set, 传递给内核,内核进行遍历(相对于NIO,这种轮询遍历减少了用户态到内核态的切换,也就是系统调用的次数)
  • 问题点:1.重复传递fd 解决方案:内核开辟空间保留fd。 2.每次select、poll,都要重新遍历全量的fd 解决方案:中断,callback,回调函数

4. epoll

  有如下服务端代码

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

public class SocketMultiplexingSingleThreadv1 {

    private ServerSocketChannel server = null;

    private Selector selector = null;

    int port = 9090;

    public void initServer() {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));

            selector = Selector.open(); // 默认选择顺序  内核epoll 内核poll 内核selector
            server.register(selector, SelectionKey.OP_ACCEPT);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        initServer();
        System.out.println("服务器启动了。。。。。。");
        try {
            while (true) {
                Set<SelectionKey> keys = selector.keys();
               // System.out.println(keys.size() + "  size");
                while (selector.select(500) > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        if (key.isAcceptable()) {
                            acceptHandler(key);
                        }else if (key.isReadable()) {
                            readHandler(key);
                        }
                    }
                }
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel client = ssc.accept();
            client.configureBlocking(false);

            ByteBuffer buffer = ByteBuffer.allocate(8192);
            client.register(selector,SelectionKey.OP_READ,buffer);

            System.out.println("-----------------------");
            System.out.println("新客户端:" + client.getRemoteAddress());
            System.out.println("-----------------------");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readHandler(SelectionKey key) {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        int read = 0;
        try {
            while (true) {
                read = client.read(buffer);
                if (read > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                }else if(read == 0) {
                    break;
                }else {
                    client.close();
                    break;
                }
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SocketMultiplexingSingleThreadv1 singleThreadv1 = new SocketMultiplexingSingleThreadv1();
        singleThreadv1.start();
    }
}

我们将该代码放在linux系统上进行编译,执行如下命令

strace -ff -o out java SocketMultiplexingSingleThreadv1

在这里插入图片描述

有这些线程启动

在这里插入图片描述

执行如下命令,查看主线程调用情况

vi out.12451
// 调用内核socket得到文件描述符8
socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 8
// 设置socket8为 非阻塞
fcntl(8, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
// 绑定文件描述符8 到端口9090
bind(8, {sa_family=AF_INET6, sin6_port=htons(9090), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::"        , &sin6_addr), sin6_scope_id=0}, 28) = 0
// 监听8
listen(8, 50)    
// 创建多路复用器 epoll 文件描述符为11
epoll_create(256)                       = 11

epoll_ctl(11, EPOLL_CTL_ADD, 9, {EPOLLIN, {u32=9, u64=16729297221677219849}}) = 0
// 把文件描述符8 注册到epoll 11
epoll_ctl(11, EPOLL_CTL_ADD, 8, {EPOLLIN, {u32=8, u64=18295631872807403528}}) = 0
// 相当于java的selector.select(500)/selector.selectedKeys();方法
epoll_wait(11, [], 8192, 500)           = 0

另开一页,执行如下命令,连接服务端

nc localhost 8090

查看主线程日志

// 一个io通道准备就绪
epoll_wait(11, [{EPOLLIN, {u32=8, u64=1115190942760960008}}], 8192, 500) = 1
// 与客户端建立socket连接 12是唯一文件描述符
accept(8, {sa_family=AF_INET6, sin6_port=htons(52612), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_scope_id=0}, [28]) = 12
// 把文件描述符12,注册到epoll 11
epoll_ctl(11, EPOLL_CTL_ADD, 12, {EPOLLIN, {u32=12, u64=12}}) = 0

在这里插入图片描述

  应用层将 Socket 连接通路通过 register 方法注入到内核 epoll 上,这些 IO 通道如果有客户端连接进来 or 数据发送进来,epoll 将它们 copy 到结果集空间,我们应用层调用 selector.select(50) 方法去检验结果集空间是否有准备的 Socket 连接通路。这里 cpu01 专门负责内核相关事情,而 cpu02 负责应用层方面,可以做到并行异步的效果。

  • epoll怎 么知道你的 Socket 连接通路上有数据了,同时让你的程序开始处理你的数据?(cpu01复制通道到结果集空间) 答:网卡会发一个中断号给你的 cpu01,而不用 cpu01 轮询检查每个连接通路是否有数据过来。
  • 问题点:应用层的服务端用户是单线程编码,如果客户端有10w个同时发送数据,单线程处理起来难免吃力。

  epoll多线程java代码

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.util.Iterator;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;

public class SocketMultiplexingThreads {

    private ServerSocketChannel server = null;

    private Selector selector0 = null;

    private Selector selector1 = null;

    private Selector selector2 = null;

    int prot = 9090;

    public void initServer() {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(prot));
            selector0 = Selector.open();
            selector1 = Selector.open();
            selector2 = Selector.open();
            server.register(selector0, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SocketMultiplexingThreads service = new SocketMultiplexingThreads();
        service.initServer();
        NioThread t1 = new NioThread(service.selector0, 2);
        NioThread t2 = new NioThread(service.selector1 );
        NioThread t3 = new NioThread(service.selector2);

        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
        t3.start();

    }

    public static class NioThread extends Thread {

        Selector selector = null;

        static int selectors = 0;

        int id = 0;

        volatile static BlockingQueue<SocketChannel>[] queue;

        static AtomicInteger idx = new AtomicInteger();

        NioThread(Selector sel, int n) {
            NioThread.this.selector = sel;
            NioThread.selectors = n;

            queue = new LinkedBlockingDeque[selectors];
            for (int i = 0; i < n; i++) {
                queue[i] =  new LinkedBlockingDeque<>();
            }
            System.out.println("Boss 启动");
        }

        NioThread(Selector sel) {
            this.selector = sel;
            id = idx.getAndIncrement() % selectors;
            System.out.println("worker:" + id + "启动");

        }

        @Override
        public void run() {
            try {
                while (true) {
                    while (selector.select(10) > 0) {
                        Set<SelectionKey> selectionKeys = selector.selectedKeys();
                        Iterator<SelectionKey> iter = selectionKeys.iterator();
                        while (iter.hasNext()) {
                            SelectionKey key = iter.next();
                            iter.remove();
                            if (key.isAcceptable()) {
                                acceptHandler(key);
                            }else if (key.isReadable()) {
                                readHandler(key);
                            }
                        }
                    }
                    if (!queue[id].isEmpty()) {
                        ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
                        SocketChannel client = queue[id].take();
                        client.register(selector,SelectionKey.OP_READ, buffer); // 把client连接,也就是内核fd 注册到epoll上
                        System.out.println("---------------------");
                        System.out.println("新客户端:" + client.socket().getPort() + "分配到:" + id);
                        System.out.println("---------------------");
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public void acceptHandler(SelectionKey key) {
            try {
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel client = ssc.accept(); // 与客户端建立连接,生成内核fd
                client.configureBlocking(false);

                int num = idx.getAndIncrement() % selectors;

                queue[num].add(client);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public void readHandler(SelectionKey key) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
            int read = 0;
            try {
                while (true) {
                    read = client.read(buffer);
                    if (read > 0) {
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            client.write(buffer);
                        }
                    }else if(read == 0) {
                        break;
                    }else {
                        client.close();
                        break;
                    }
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

5. netty

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.nio.charset.Charset;

public class NettyIO {

    public static void main(String[] args) {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);  // accpet 接受线程
        NioEventLoopGroup worker = new NioEventLoopGroup(5); // recfron 读取线程

        // 服务端启动引导
        ServerBootstrap boot = new ServerBootstrap();

        try {
            boot.group(boss, worker) // 绑定两个程序组
                    .channel(NioServerSocketChannel.class) // 指定通道类型 nio
                    .option(ChannelOption.TCP_NODELAY, false) // 设置tcp
                    .childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(Channel channel) throws Exception {
                            ChannelPipeline p = channel.pipeline(); // 获取处理器链
                            p.addLast(new MyInbuund()); // 添加消息处理事件
                        }
                    })
                    .bind(9999) // 绑定断开
                    .sync()
                    .channel() // 阻塞主线程,知道网络服务被关闭
                    .closeFuture()
                    .sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class MyInbuund extends ChannelInboundHandlerAdapter {
    // 每当从客户端收到新的数据时,这个方法会在收到消息时被调用
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("收到数据:" + ((ByteBuf) msg).toString(Charset.defaultCharset()));
        ctx.write(Unpooled.wrappedBuffer("Server message".getBytes()));
        ctx.fireChannelRead(msg);
    }

    // 数据读取完后被调用
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    // 当Netty由于IO错误或者处理器在处理事件时抛出的异常时被调用
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值