首页 > 代码库 > Mina、Netty、Twisted一起学(六):session
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连接的相关信息。
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