淺談netty

要理解netty,我們需要先了解I/O Models和JAVA NIO,還有觀察者模式、多Reactors線程模型等等這些內容。

I/O Models

在這里我們先要回顧一些操作系統(tǒng)的IO相關基礎知識:

  • 用戶空間與內核空間:
    現(xiàn)在操作系統(tǒng)都是采用虛擬存儲器,那么對32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。操作系統(tǒng)的核心是內核,獨立于普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操心系統(tǒng)將虛擬空間劃分為兩部分,一部分為內核空間,一部分為用戶空間。針對linux操作系統(tǒng)而言,將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱為內核空間,而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。
  • 系統(tǒng)調用:
    Linux/Unix內核中設置了一組用于實現(xiàn)各種系統(tǒng)功能的子程序,稱為系統(tǒng)調用。用戶可以通過系統(tǒng)調用命令在自己的應用程序中調用它們。從某種角度來看,系統(tǒng)調用和普通的函數(shù)調用非常相似。區(qū)別僅僅在于,系統(tǒng)調用由操作系統(tǒng)核心提供,運行于核心態(tài);而普通的函數(shù)調用由函數(shù)庫或用戶自己提供,運行于用戶態(tài)。
  • 進程切換:
    1.為了控制進程的執(zhí)行,內核必須有能力掛起正在CPU上運行的進程,并恢復以前掛起的某個進程的執(zhí)行。這種行為被稱為進程切換。因此可以說,任何進程都是在操作系統(tǒng)內核的支持下運行的,是與內核緊密相關的。
    2.消耗CPU資源。
  • 進程的阻塞:
    1.正在執(zhí)行的進程,由于期待的某些事件未發(fā)生,如請求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據尚未到達或無新工作做等,則由系統(tǒng)自動執(zhí)行阻塞原語(Block),使自己由運行狀態(tài)變?yōu)樽枞麪顟B(tài)??梢?,進程的阻塞是進程自身的一種主動行為,也因此只有處于運行態(tài)的進程(獲得CPU),才可能將其轉為阻塞狀態(tài)。
    2.當進程進入阻塞狀態(tài),不消耗CPU資源。
  • 文件描述符:
    1.文件描述符(File descriptor)是計算機科學中的一個術語,是一個用于表述指向文件的引用的抽象化概念。
    2.文件描述符在形式上是一個非負整數(shù)。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現(xiàn)有文件或者創(chuàng)建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)。
  • 緩存 I/O:
    1.緩存 I/O 又被稱作標準 I/O,大多數(shù)文件系統(tǒng)的默認 I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統(tǒng)會將 I/O 的數(shù)據緩存在文件系統(tǒng)的頁緩存( page cache )中,也就是說,數(shù)據會先被拷貝到操作系統(tǒng)內核的緩沖區(qū)中,然后才會從操作系統(tǒng)內核的緩沖區(qū)拷貝到應用程序的地址空間。
    2.缺點:數(shù)據在傳輸過程中需要在應用程序地址空間和內核進行多次數(shù)據拷貝操作,這些數(shù)據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。

當一個輸入(input)操作發(fā)生時,這里會經歷兩個不同的階段:
1.等待數(shù)據就緒。
2.將數(shù)據從內核的緩沖區(qū)拷貝到進程中。

下面是5種IO模型
1.blocking I/O
2.nonblocking I/O
3.I/O multiplexing (select and poll)
4.signal driven I/O (SIGIO)
5.asynchronous I/O (the POSIX aio_functions)

Blocking I/O Model
image.png

整個讀取IO的數(shù)據是同步、阻塞的。

Nonblocking I/O Model
image.png

當一個應用程序像這樣對一個非阻塞描述符循環(huán)調用recvfrom時,我們稱之為輪詢(polling),應用程序持續(xù)輪詢內核,以查看某個操作是否就緒,這么做往往耗費大量CPU時間。通常是在專門提供某一種功能的系統(tǒng)中才有

I/O Multiplexing Model
image.png
  • 在IO復用中,我們調用select或者poll 然后在這兩個系統(tǒng)調用中的一個中阻塞,而不是阻塞在真實的I/O系統(tǒng)調用上。
  • 優(yōu)勢:跟阻塞I/O模型比較使用select我們可以等待多個描述符就緒,也就是一個線程可以處理多個描述符。
signal driven I/O (SIGIO)
image.png
  • 我們也可以用信號,讓內核在描述符就緒時發(fā)送SIGIO信號通知我們。
  • 優(yōu)勢:等待數(shù)據報到達期間進程不被阻塞。主循環(huán)可以繼續(xù)執(zhí)行,只要等待來自信號處理函數(shù)的通知(既可以是數(shù)據已準備好被處理,也可以是數(shù)據報已準備好被讀取。)
asynchronous I/O (the POSIX aio_functions)

告知內核啟動某個操作,并讓內核在整個操作(包括將數(shù)據從內核復制到我們自己的緩沖區(qū))完成后通知我們。信號驅動式I/O是由內核通知我們何時可以啟動一個I/O操作,而異步I/O模型是由內核通知我們I/O操作何時完成。通過狀態(tài)、通知和回調來通知調用者的輸入輸出操作。

image.png
  • 優(yōu)勢:
    我們調用aio_read函數(shù),給內核傳遞描述符、緩沖區(qū)指針、緩沖區(qū)大小和文件偏移,病告訴內核當整個操作完成時如何通知我們。該系統(tǒng)調用立即返回,而且在等待I/O完成期間,我們的進程不被阻塞。
各種I/O模型的比較
image.png
  • 前4中模型的主要區(qū)別在于第一階段,因為它們的第二階段是一樣的:在數(shù)據從內核復制到調用者的緩沖區(qū)期間,進程阻塞于recvfrom調用。
  • 同步I/O操作導致請求進程阻塞,直到I/O操作完成,異步I/O操作不導致請求進程阻塞。前四種都是同步I/O模型,因為其中的I/O操作recvfrom講阻塞進程。只有異步I/O模型與POSIX定義的異步I/O相匹配。

隨著linux內核的不斷發(fā)展,IO也在不斷發(fā)展,所以后面有了IO多路復用模型。IO 多路復用是通過linux內核的select、poll、epoll這些來完成的。

select函數(shù)

該函數(shù)允許進程指示內核等待多個事件中的任何一個發(fā)生,并只在有一個或多個事件發(fā)生或經歷一段指定的時間后才喚醒它。
例如:我們可以調用select,告知內核僅在下列情況發(fā)生時才返回:

  • 集合{1,4,5}中的任何描述符準備好讀;
  • 集合{2,7}中任何描述符準備好寫;
  • 集合{1,4}中的任何描述符有異常條件待處理;
  • 已經歷了10.2秒
    下面是select函數(shù)的簡單結構
#include <sys/select.h>
#include <sys/time.h>

//返回:若有就緒描述符則為其數(shù)目,若超時則為0,若出錯則為-1
int select(int maxfdp1,fd_set *readset,fd_set *writeset, fd_set *exceptest,const struct timeval *timeout);

struct timeval{
    long tv_sec;       /* seconds */
    long tv_used;    /* microseconds */
}

select以后最大的優(yōu)勢是用戶可以在一個線程內同時處理多個socket的IO請求
pselect : 能夠處理信號阻塞并提供了更高時間分辨率的select的增強版本

poll函數(shù)
#include <poll.h>

//返回:若有就緒描述符則為其數(shù)目,若超時則為0,若出錯則為-1
int poll (struct pollfd *fdarray, unsigned long nfds, int timeout);

//一個pollfd結構體表示一個被監(jiān)視的文件描述符
struct pollfd {
int fd; /* descriptor to check */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
};

select機制的問題
1.每次調用select,都需要把fd_set集合從用戶態(tài)拷貝到內核態(tài),如果fd_set集合很大時,那這個開銷也很大
2.同時每次調用select都需要在內核遍歷傳遞進來的所有fd_set,如果fd_set集合很大時,那這個開銷也很大
3.為了減少數(shù)據拷貝帶來的性能損壞,內核對被監(jiān)控的fd_set集合大小做了限制,并且這個是通過宏控制的,大小不可改變(限制為1024) 【poll用數(shù)組結構體解決了大小限制問題】

它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的,但是同樣有一些缺點:

  • 大量的fd的數(shù)組被整體復制于用戶態(tài)和內核地址空間之間,而不管這樣的復制是不是有意義。
  • poll還有一個特點是“水平觸發(fā)”,如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。
epoll函數(shù)

epoll在Linux2.6內核正式提出,是基于事件驅動的I/O方式,相對于select來說,epoll沒有描述符個數(shù)限制,使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

int epoll_create(int size);  // epoll_create 函數(shù)創(chuàng)建一個epoll句柄
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 函數(shù)注冊要監(jiān)聽的事件類型
//  epoll_wait 函數(shù)等待事件的就緒,成功時返回就緒的事件數(shù)目,調用失敗時返回 -1,等待超時返回 0
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
struct epoll_event {    
    __uint32_t events;  /* Epoll events */    
    epoll_data_t data;  /* User data variable */};

typedef union epoll_data {    
   void *ptr;   
    int fd;    
    __uint32_t u32;   
    __uint64_t u64;
} epoll_data_t;

epoll優(yōu)點:
1.沒有最大并發(fā)連接的限制,能打開的FD的上限遠大于1024(1G的內存上能監(jiān)聽約10萬個端口);
2.效率提升,不是輪詢的方式,不會隨著FD數(shù)目的增加效率下降。只有活躍可用的FD才會調用callback函數(shù);即Epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數(shù)無關,因此在實際的網絡環(huán)境中,Epoll的效率就會遠遠高于select和poll。

  1. 內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞;即epoll使用mmap減少復制開銷。

它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率。原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。

對比:


image.png
總結

綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。

1.表面上看epoll的性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數(shù)回調。

2.select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善。

JAVA NIO

Java NIO提供了與標準IO不同的IO工作方式:

  • Channels and Buffers(通道和緩沖區(qū)):
    標準的IO基于字節(jié)流和字符流進行操作的,而NIO是基于通道(Channel)和緩沖區(qū)(Buffer)進行操作,數(shù)據總是從通道讀取到緩沖區(qū)中,或者從緩沖區(qū)寫入到通道中。

  • 非阻塞IO(Non-blocking IO):
    Java NIO可以讓執(zhí)行非阻塞IO,例如:當線程從通道讀取數(shù)據到緩沖區(qū)時,線程還是可以進行其他事情。當數(shù)據被寫入到緩沖區(qū)時,線程可以繼續(xù)處理它。從緩沖區(qū)寫入通道也類似。

  • 選擇器(Selectors):
    Java NIO引入了選擇器的概念,選擇器用于監(jiān)聽多個通道的事件(比如:連接打開,數(shù)據到達)。因此,單個的線程可以監(jiān)聽多個數(shù)據通道。

java nio 的幾個重要組件:


image.png

Channels and Buffers

基本上,所有的 IO 在NIO 中都從一個Channel 開始。Channel 有點象流。 數(shù)據可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。


image.png

常見的幾種channel:

  • FileChannel
  • DatagramChannel UDP連接
  • SocketChannel 客戶端socket連接
  • ServerSocketChannel 服務端socket連接

常見的幾種Buffer:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
  • Mappedyteuffer 表示內存映射文件 java nio 提供了這個在某種場景下極大提升效率
selector

Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。


image.png
             A Thread uses a Selector to handle 3 Channel's 
IO VS NIO
image.png

netty原理

Netty是一個事件驅動、異步IO的網絡框架。高性能,吞吐量更高,延遲更低、高性能之處主要來自于其I/O 模型和線程處理模型(Reactor),前者決定如何收發(fā)數(shù)據,后者決定如何處理數(shù)據。


image.png
Reactor模式
image.png

Reactor模式(反應器模式)是一種處理一個或多個客戶端并發(fā)交付服務請求的事件設計模式。當請求抵達后,服務處理程序使用I/O多路復用策略,然后同步地派發(fā)這些請求至相關的請求處理程序。

核心組件交互圖如下:

image.png

  • Handle(句柄或描述符,在Windows下稱為句柄,在Linux下稱為描述符):本質上表示一種資源(比如說文件描述符,或是針對網絡編程中的socket描述符),是由操作系統(tǒng)提供的;該資源用于表示一個個的事件,事件既可以來自于外部,也可以來自于內部。
  • Synchronous Event Demultiplexer(同步事件分離器):它本身是一個系統(tǒng)調用,用于等待事件的發(fā)生(事件可能是一個,也可能是多個)。調用方在調用它的時候會被阻塞,一直阻塞到同步事件分離器上有事件產生為止。對于Linux來說,同步事件分離器指的就是常用的I/O多路復用機制,比如說select、poll、epoll等。在Java NIO領域中,同步事件分離器對應的組件就是Selector;對應的阻塞方法就是select方法。
  • Event Handler(事件處理器):本身由多個回調方法構成,這些回調方法構成了與應用相關的對于某個事件的反饋機制。在Java NIO領域中并沒有提供事件處理器機制讓我們調用或去進行回調,是由我們自己編寫代碼完成的。Netty相比于Java NIO來說,在事件處理器這個角色上進行了一個升級,它為我們開發(fā)者提供了大量的回調方法,供我們在特定事件產生時實現(xiàn)相應的回調方法進行業(yè)務邏輯的處理,即ChannelHandler。ChannelHandler中的方法對應的都是一個個事件的回調。
  • Initiation Dispatcher(初始分發(fā)器):際上就是Reactor角色。它本身定義了一些規(guī)范,這些規(guī)范用于控制事件的調度方式,同時又提供了應用進行事件處理器的注冊、刪除等設施。它本身是整個事件處理器的核心所在,Initiation Dispatcher會通過Synchronous Event Demultiplexer來等待事件的發(fā)生。一旦事件發(fā)生,Initiation Dispatcher首先會分離出每一個事件,然后調用事件處理器,最后調用相關的回調方法來處理這些事件。Netty中ChannelHandler里的一個個回調方法都是由bossGroup或workGroup中的某個EventLoop來調用的。
Basic Reactor Design
image.png
NIO實現(xiàn)Reactor
image.png

image.png
image.png

image.png

image.png

上面是用java nio實現(xiàn)基本Reactor模式,需要自己寫很多代碼。

Worker Thread Pools 版 Reactor模式
image.png
多Reactor模式
image.png

netty就是使用的就是多Reactor模式:

image.png

Netty的異步處理:

image.png

常見操作:
image.png

netty功能特性
image.png
Netty核心組件
image.png
ChannelPipeline處理入站事件和出站操作
image.png

image.png

image.png
Netty Reactor 工作架構圖
image.png

image.png

image.png

下面有一個基于netty的簡單的IM demo,可以簡單了解netty的編程方法和思想:
Server:

package com.yuanjia.im.netty.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.net.InetSocketAddress;

/**
 * Created by bruce on 2019/6/10.
 */
public class Server {
    private int port;

    public Server(int port) {
        this.port = port;
    }

    public void start() {

        // netty服務端ServerBootstrap啟動的時候,默認有兩個eventloop分別是bossGroup和 workGroup

        EventLoopGroup boosGroup = new NioEventLoopGroup(1);   // bossGroup
        EventLoopGroup workerGroup = new NioEventLoopGroup();  // workGroup
        try {
            ServerBootstrap sbs = new ServerBootstrap().group(boosGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast("decoder", new StringDecoder());
                            ch.pipeline().addLast("encoder", new StringEncoder());
                            //ch.pipeline().addLast(new DiscardInboundHandler());
                            ch.pipeline().addLast(new ServerHandler());
                        };
                    }).option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture future = sbs.bind(port).sync();
            System.out.println("Server start listen at " + port);
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8090;
        }
        new Server(port).start();
    }
}

Client:

package com.yuanjia.im.netty.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;

/**
 * Created by bruce on 2019/6/10.
 */
public class Client {

    //server 's ip 這里需要用戶根據自己server的ip來做修改,例如我這里是10.1.132.194
    private static final String HOST = System.getProperty("host", "10.1.132.194");
    //port 8090
    private static final int PORT = Integer.parseInt(System.getProperty("port", "8090"));

    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>(){
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast("decoder", new StringDecoder());
                            p.addLast("encoder", new StringEncoder());
                            p.addLast(new ClientHandler());
                        }
                    });
            ChannelFuture future = b.connect(HOST, PORT).sync();
            //控制臺輸入消息給服務端讓服務端轉給給另外一個客戶端
            //消息如:  認識你真高興我的小伙伴@10.1.8.30
            //消息就轉發(fā)給了10.1.8.30
            Scanner sc = new Scanner(System.in);
            while(sc.hasNext()){
                String message = sc.nextLine();
                future.channel().writeAndFlush(message);
            }
            future.channel().closeFuture().sync();
        } finally {
            group.spliterator();
        }
    }

}

ServerHandler:

package com.yuanjia.im.netty.server;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.util.internal.PlatformDependent;

import java.util.concurrent.ConcurrentMap;

/**
 * Created by bruce on 2019/6/10.
 */
@ChannelHandler.Sharable
public class ServerHandler implements ChannelInboundHandler {

    //存放客戶端和服務端之間的連接
    private static ConcurrentMap<String,ChannelHandlerContext> channelConcurrentMap = PlatformDependent.newConcurrentHashMap();


    @Override
    public void channelRegistered(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void channelUnregistered(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void channelActive(ChannelHandlerContext channelHandlerContext) throws Exception {
        //獲取客戶端的ip
        String hostString = ((SocketChannel)channelHandlerContext.channel()).remoteAddress().getHostString();
        System.out.println(hostString + " online");
        //將客戶端和服務端之間的連接存放在concurrentHashMap中
        channelConcurrentMap.put(hostString,channelHandlerContext);
    }

    @Override
    public void channelInactive(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void channelRead(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
        System.out.println("ServerHandler channelRead....");
        //客戶端通過Terminal連接后的輸入格式為  message@ip ,這個消息接收者ip會收到message消息
        //例如:   你最近還好嗎,Bruce@10.1.128.1
        String messageString = o.toString();
        String[] messages = messageString.split("@");
        String message = messages[0];
        String targetHost = messages[1];
        System.out.println(channelHandlerContext.channel().remoteAddress()+"->Server :"+o.toString());
        ChannelHandlerContext targetChannelHandlerContext = channelConcurrentMap.get(targetHost);
        targetChannelHandlerContext.write(channelHandlerContext.channel().remoteAddress() + " say : " + message);
        targetChannelHandlerContext.flush();
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void userEventTriggered(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {

    }

    @Override
    public void channelWritabilityChanged(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void handlerAdded(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void handlerRemoved(ChannelHandlerContext channelHandlerContext) throws Exception {

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable throwable) throws Exception {

    }
}

ClientHandler

package com.yuanjia.im.netty.client;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * Created by bruce on 2019/6/10.
 */
public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Client01Handler Active");
        //ctx.fireChannelActive();  // 若把這一句注釋掉將無法將event傳遞給下一個ClientHandler
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println(msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

代碼地址:https://github.com/bruceChi/nettyIM
參考資料:
1、http://tutorials.jenkov.com/java-nio
2、 UNIX Network Programming
3、 http://www.itdecent.cn/p/63a006e5e22d
4、http://tutorials.jenkov.com/netty
5、http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf
6、 https://www.cnblogs.com/winner-0715/p/8733787.html
7、 http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容