Mina、Netty、Twisted一起學(六):session


開發過Web應用的同學應該都會使用session。由於HTTP協議本身是無狀態的,所以一個客戶端多次訪問這個web應用的多個頁面,服務器無法判斷多次訪問的客戶端是否是同一個客戶端。有了session就可以設置一些和客戶端相關的屬性,用於保持這種連接狀態。例如用戶登錄系統后,設置session標記這個客戶端已登錄,那么訪問別的頁面時就不用再次登錄了。

不過本文的內容不是Web應用的session,而是TCP連接的session,實際上二者還是有很大區別的。Web應用的session實現方式並不是基於同一個TCP連接,而是通過cookie實現,這里不再詳細展開。上面講到Web應用的session只是讓大家理解session的概念。

在同步阻塞的網絡編程中,代碼都是按照TCP操作順序編寫的,即創建連接、多次讀寫、關閉連接,這樣很容易判斷這一系列操作是否是同一個連接。而在事件驅動的異步網絡編程框架中,IO操作都會觸發一個事件調用相應的事件函數,例如接收到客戶端的新數據,會調用messageReceived(MINA)、channelRead(Netty)、dataReceived(Twisted),同一個TCP連接的多次請求和多個客戶端請求都是一樣的。

那么如何判斷多次請求到底是不是同一個TCP連接,如何保存連接相關的信息?針對這個問題,MINA、Netty、Twisted都提供了相應的解決方案。

下面分別用MINA、Netty、Twisted實現一個請求次數計數器,用於記錄同一個連接多次請求的請求次數。

MINA:

在MINA中,每當一個客戶端連接到服務器,就會創建一個新的IoSession,直到客戶端斷開連接才會銷毀。IoSession可以用setAttribute和getAttribute來存儲和獲取一個TCP連接的相關信息。

MINA官方文檔對IoSession的解釋:

The Session is at the heart of MINA : every time a client connects to the server, a new session is created, and will be kept in memory until the client is disconnected.
A session is used to store persistent informations about the connection, plus any kind of information the server might need to use during the request processing, and eventually during the whole session life.

public class TcpServer {

    public static void main(String[] args) throws IOException {
        IoAcceptor acceptor = new NioSocketAcceptor();

        acceptor.getFilterChain().addLast("codec",
                new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"), "\r\n", "\r\n")));

        acceptor.setHandler(new TcpServerHandle());
        acceptor.bind(new InetSocketAddress(8080));
    }

}

class TcpServerHandle extends IoHandlerAdapter {

    @Override
    public void exceptionCaught(IoSession session, Throwable cause)
            throws Exception {
        cause.printStackTrace();
    }

    // 接收到新的數據
    @Override
    public void messageReceived(IoSession session, Object message)
            throws Exception {

        int counter = 1;
        
        // 第一次請求,創建session中的counter
        if(session.getAttribute("counter") == null) {
            session.setAttribute("counter", 1);
        } else {
            // 獲取session中的counter,加1后再存入session
            counter = (Integer) session.getAttribute("counter");
            counter++;
            session.setAttribute("counter", counter);
        }
        
        String line = (String) message;
        System.out.println("第" + counter + "次請求:" + line);
    }
}

Netty:

Netty中分為兩種情況,一種是針對每個TCP連接創建一個新的ChannelHandler實例,另一種是所有TCP連接共用一個ChannelHandler實例。這兩種方式的區別在於ChannelPipeline的addLast方法中添加的是否是新的ChannelHandler實例。

針對每個TCP連接創建一個新的ChannelHandler實例:

針對每個TCP連接創建一個新的ChannelHandler實例是最常用的一種方式。這種情況非常簡單,直接在ChannelHandler的實現類中加入一個成員變量即可保存連接相關的信息。

這也是Netty官方文檔中推薦的一種方式,不過要保證針對每個連接創建新的ChannelHandler實例:

A ChannelHandler often needs to store some stateful information. The simplest and recommended approach is to use member variables.
Because the handler instance has a state variable which is dedicated to one connection, you have to create a new handler instance for each new channel to avoid a race condition where a unauthenticated client can get the confidential information.

public class TcpServer {

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new LineBasedFrameDecoder(80));
                            pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new TcpServerHandler()); // 針對每個TCP連接創建一個新的ChannelHandler實例
                        }
                    });
            ChannelFuture f = b.bind(8080).sync();
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

}

class TcpServerHandler extends ChannelInboundHandlerAdapter {

    // 連接相關的信息直接保存在TcpServerHandler的成員變量中
    private int counter = 0;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        
        counter++;
        
        String line = (String) msg;
        System.out.println("第" + counter + "次請求:" + line);
    }

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

所有TCP連接共用一個ChannelHandler實例:

在這種情況下,就不能把連接相關的信息放在ChannelHandler實現類的成員變量中了,否則這些信息會被其他連接共用。這里就要使用到ChannelHandlerContext的Attribute了。

Netty文檔節選:

Although it's recommended to use member variables to store the state of a handler, for some reason you might not want to create many handler instances. In such a case, you can use AttributeKeys which is provided by ChannelHandlerContext.

public class TcpServer {

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        
                        private TcpServerHandler tcpServerHandler = new TcpServerHandler();
                        
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new LineBasedFrameDecoder(80));
                            pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                            pipeline.addLast(tcpServerHandler); // 多個連接使用同一個ChannelHandler實例
                        }
                    });
            ChannelFuture f = b.bind(8080).sync();
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

}

@Sharable // 多個連接使用同一個ChannelHandler,要加上@Sharable注解
class TcpServerHandler extends ChannelInboundHandlerAdapter {
    
    private AttributeKey<Integer> attributeKey = AttributeKey.valueOf("counter");

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        
        Attribute<Integer> attribute = ctx.attr(attributeKey);
        
        int counter = 1;
        
        if(attribute.get() == null) {
            attribute.set(1);
        } else {
            counter = attribute.get();
            counter++;
            attribute.set(counter);
        }
        
        String line = (String) msg;
        System.out.println("第" + counter + "次請求:" + line);
    }

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

Twisted:

在Twisted中,每個TCP連接都會創建一個新的Protocol實例,這樣也就很簡單了,直接將連接相關的信息保存為Protocol繼承類的屬性。

Twisted文檔節選:

An instance of the protocol class is instantiated per-connection, on demand, and will go away when the connection is finished.

# -*- coding:utf-8 –*-

from twisted.protocols.basic import LineOnlyReceiver
from twisted.internet.protocol import Factory
from twisted.internet import reactor

class TcpServerHandle(LineOnlyReceiver):

    # 連接相關的信息直接保存為Protocol繼承類TcpServerHandle的屬性
    counter = 0;

    def lineReceived(self, data):
        self.counter += 1
        print "" + str(self.counter) + "次請求:" + data

factory = Factory()
factory.protocol = TcpServerHandle
reactor.listenTCP(8080, factory)
reactor.run()

下面是一個Java實現的客戶端,代碼中發起了3次TCP連接,在每個連接中發送兩次請求數據到服務器:

public class TcpClient {

    public static void main(String[] args) throws IOException, InterruptedException {

        // 3次TCP連接,每個連接發送2個請求數據
        for(int i = 0; i < 3; i++) {
            
            
            Socket socket = null;
            OutputStream out = null;
    
            try {
    
                socket = new Socket("localhost", 8080);
                out = socket.getOutputStream();
    
                // 第一次請求服務器
                String lines1 = "Hello\r\n";
                byte[] outputBytes1 = lines1.getBytes("UTF-8");
                out.write(outputBytes1);
                out.flush();
    
                // 第二次請求服務器
                String lines2 = "World\r\n";
                byte[] outputBytes2 = lines2.getBytes("UTF-8");
                out.write(outputBytes2);
                out.flush();
    
            } finally {
                // 關閉連接
                out.close();
                socket.close();
            }
            
            Thread.sleep(1000);
        }
    }
}

分別測試上面的4個服務器,輸出結果都是:

第1次請求:Hello
第2次請求:World
第1次請求:Hello
第2次請求:World
第1次請求:Hello
第2次請求:World

MINA、Netty、Twisted一起學系列

MINA、Netty、Twisted一起學(一):實現簡單的TCP服務器

MINA、Netty、Twisted一起學(二):TCP消息邊界問題及按行分割消息

MINA、Netty、Twisted一起學(三):TCP消息固定大小的前綴(Header)

MINA、Netty、Twisted一起學(四):定制自己的協議

MINA、Netty、Twisted一起學(五):整合protobuf

MINA、Netty、Twisted一起學(六):session

MINA、Netty、Twisted一起學(七):發布/訂閱(Publish/Subscribe)

MINA、Netty、Twisted一起學(八):HTTP服務器

MINA、Netty、Twisted一起學(九):異步IO和回調函數

MINA、Netty、Twisted一起學(十):線程模型

MINA、Netty、Twisted一起學(十一):SSL/TLS

MINA、Netty、Twisted一起學(十二):HTTPS

源碼

https://github.com/wucao/mina-netty-twisted


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM