標籤:style blog http color io os 使用 java ar
開發過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串連的相關資訊。
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的實作類別中加入一個成員變數即可儲存串連相關的資訊。
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了。
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繼承類的屬性。
# -*- coding:utf-8 –*-from twisted.protocols.basic import LineOnlyReceiverfrom twisted.internet.protocol import Factoryfrom twisted.internet import reactorclass TcpServerHandle(LineOnlyReceiver): def lineReceived(self, data): if(hasattr(self, ‘counter‘)): self.counter += 1 else: self.counter = 1; print "第" + str(self.counter) + "次請求:" + datafactory = Factory()factory.protocol = TcpServerHandlereactor.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一起學(六):session