开发过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.

  1. public class TcpServer {
  2.  
  3. public static void main(String[] args) throws IOException {
  4. IoAcceptor acceptor = new NioSocketAcceptor();
  5.  
  6. acceptor.getFilterChain().addLast("codec",
  7. new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"), "\r\n", "\r\n")));
  8.  
  9. acceptor.setHandler(new TcpServerHandle());
  10. acceptor.bind(new InetSocketAddress(8080));
  11. }
  12.  
  13. }
  14.  
  15. class TcpServerHandle extends IoHandlerAdapter {
  16.  
  17. @Override
  18. public void exceptionCaught(IoSession session, Throwable cause)
  19. throws Exception {
  20. cause.printStackTrace();
  21. }
  22.  
  23. // 接收到新的数据
  24. @Override
  25. public void messageReceived(IoSession session, Object message)
  26. throws Exception {
  27.  
  28. int counter = 1;
  29.  
  30. // 第一次请求,创建session中的counter
  31. if(session.getAttribute("counter") == null) {
  32. session.setAttribute("counter", 1);
  33. } else {
  34. // 获取session中的counter,加1后再存入session
  35. counter = (Integer) session.getAttribute("counter");
  36. counter++;
  37. session.setAttribute("counter", counter);
  38. }
  39.  
  40. String line = (String) message;
  41. System.out.println("第" + counter + "次请求:" + line);
  42. }
  43. }

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.

  1. public class TcpServer {
  2.  
  3. public static void main(String[] args) throws InterruptedException {
  4. EventLoopGroup bossGroup = new NioEventLoopGroup();
  5. EventLoopGroup workerGroup = new NioEventLoopGroup();
  6. try {
  7. ServerBootstrap b = new ServerBootstrap();
  8. b.group(bossGroup, workerGroup)
  9. .channel(NioServerSocketChannel.class)
  10. .childHandler(new ChannelInitializer<SocketChannel>() {
  11. @Override
  12. public void initChannel(SocketChannel ch) throws Exception {
  13. ChannelPipeline pipeline = ch.pipeline();
  14. pipeline.addLast(new LineBasedFrameDecoder(80));
  15. pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
  16. pipeline.addLast(new TcpServerHandler()); // 针对每个TCP连接创建一个新的ChannelHandler实例
  17. }
  18. });
  19. ChannelFuture f = b.bind(8080).sync();
  20. f.channel().closeFuture().sync();
  21. } finally {
  22. workerGroup.shutdownGracefully();
  23. bossGroup.shutdownGracefully();
  24. }
  25. }
  26.  
  27. }
  28.  
  29. class TcpServerHandler extends ChannelInboundHandlerAdapter {
  30.  
  31. // 连接相关的信息直接保存在TcpServerHandler的成员变量中
  32. private int counter = 0;
  33.  
  34. @Override
  35. public void channelRead(ChannelHandlerContext ctx, Object msg) {
  36.  
  37. counter++;
  38.  
  39. String line = (String) msg;
  40. System.out.println("第" + counter + "次请求:" + line);
  41. }
  42.  
  43. @Override
  44. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
  45. cause.printStackTrace();
  46. ctx.close();
  47. }
  48. }

所有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.

  1. public class TcpServer {
  2.  
  3. public static void main(String[] args) throws InterruptedException {
  4. EventLoopGroup bossGroup = new NioEventLoopGroup();
  5. EventLoopGroup workerGroup = new NioEventLoopGroup();
  6. try {
  7. ServerBootstrap b = new ServerBootstrap();
  8. b.group(bossGroup, workerGroup)
  9. .channel(NioServerSocketChannel.class)
  10. .childHandler(new ChannelInitializer<SocketChannel>() {
  11.  
  12. private TcpServerHandler tcpServerHandler = new TcpServerHandler();
  13.  
  14. @Override
  15. public void initChannel(SocketChannel ch) throws Exception {
  16. ChannelPipeline pipeline = ch.pipeline();
  17. pipeline.addLast(new LineBasedFrameDecoder(80));
  18. pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
  19. pipeline.addLast(tcpServerHandler); // 多个连接使用同一个ChannelHandler实例
  20. }
  21. });
  22. ChannelFuture f = b.bind(8080).sync();
  23. f.channel().closeFuture().sync();
  24. } finally {
  25. workerGroup.shutdownGracefully();
  26. bossGroup.shutdownGracefully();
  27. }
  28. }
  29.  
  30. }
  31.  
  32. @Sharable // 多个连接使用同一个ChannelHandler,要加上@Sharable注解
  33. class TcpServerHandler extends ChannelInboundHandlerAdapter {
  34.  
  35. private AttributeKey<Integer> attributeKey = AttributeKey.valueOf("counter");
  36.  
  37. @Override
  38. public void channelRead(ChannelHandlerContext ctx, Object msg) {
  39.  
  40. Attribute<Integer> attribute = ctx.attr(attributeKey);
  41.  
  42. int counter = 1;
  43.  
  44. if(attribute.get() == null) {
  45. attribute.set(1);
  46. } else {
  47. counter = attribute.get();
  48. counter++;
  49. attribute.set(counter);
  50. }
  51.  
  52. String line = (String) msg;
  53. System.out.println("第" + counter + "次请求:" + line);
  54. }
  55.  
  56. @Override
  57. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
  58. cause.printStackTrace();
  59. ctx.close();
  60. }
  61. }

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.

  1. # -*- coding:utf-8 –*-
  2.  
  3. from twisted.protocols.basic import LineOnlyReceiver
  4. from twisted.internet.protocol import Factory
  5. from twisted.internet import reactor
  6.  
  7. class TcpServerHandle(LineOnlyReceiver):
  8.  
  9. # 连接相关的信息直接保存为Protocol继承类TcpServerHandle的属性
  10. counter = 0;
  11.  
  12. def lineReceived(self, data):
  13. self.counter += 1
  14. print "第" + str(self.counter) + "次请求:" + data
  15.  
  16. factory = Factory()
  17. factory.protocol = TcpServerHandle
  18. reactor.listenTCP(8080, factory)
  19. reactor.run()

下面是一个Java实现的客户端,代码中发起了3次TCP连接,在每个连接中发送两次请求数据到服务器:

  1. public class TcpClient {
  2.  
  3. public static void main(String[] args) throws IOException, InterruptedException {
  4.  
  5. // 3次TCP连接,每个连接发送2个请求数据
  6. for(int i = 0; i < 3; i++) {
  7.  
  8. Socket socket = null;
  9. OutputStream out = null;
  10.  
  11. try {
  12.  
  13. socket = new Socket("localhost", 8080);
  14. out = socket.getOutputStream();
  15.  
  16. // 第一次请求服务器
  17. String lines1 = "Hello\r\n";
  18. byte[] outputBytes1 = lines1.getBytes("UTF-8");
  19. out.write(outputBytes1);
  20. out.flush();
  21.  
  22. // 第二次请求服务器
  23. String lines2 = "World\r\n";
  24. byte[] outputBytes2 = lines2.getBytes("UTF-8");
  25. out.write(outputBytes2);
  26. out.flush();
  27.  
  28. } finally {
  29. // 关闭连接
  30. out.close();
  31. socket.close();
  32. }
  33.  
  34. Thread.sleep(1000);
  35. }
  36. }
  37. }

分别测试上面的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

Mina、Netty、Twisted一起学(六):session的更多相关文章

  1. Mina、Netty、Twisted一起学(八):HTTP服务器

    HTTP协议应该是目前使用最多的应用层协议了,用浏览器打开一个网站就是使用HTTP协议进行数据传输. HTTP协议也是基于TCP协议,所以也有服务器和客户端.HTTP客户端一般是浏览器,当然还有可能是 ...

  2. Mina、Netty、Twisted一起学(十):线程模型

    要想开发一个高性能的TCP服务器,熟悉所使用框架的线程模型非常重要.MINA.Netty.Twisted本身都是高性能的网络框架,如果再搭配上高效率的代码,才能实现一个高大上的服务器.但是如果不了解它 ...

  3. Mina、Netty、Twisted一起学(九):异步IO和回调函数

    用过JavaScript或者jQuery的同学都知道,JavaScript特别是jQuery中存在大量的回调函数,例如Ajax.jQuery的动画等. $.get(url, function() { ...

  4. Mina、Netty、Twisted一起学(七):发布/订阅(Publish/Subscribe)

    消息传递有很多种方式,请求/响应(Request/Reply)是最常用的.在前面的博文的例子中,很多都是采用请求/响应的方式,当服务器接收到消息后,会立即write回写一条消息到客户端.HTTP协议也 ...

  5. Mina、Netty、Twisted一起学(五):整合protobuf

    protobuf是谷歌的Protocol Buffers的简称,用于结构化数据和字节码之间互相转换(序列化.反序列化),一般应用于网络传输,可支持多种编程语言. protobuf如何使用这里不再介绍, ...

  6. Mina、Netty、Twisted一起学(三):TCP消息固定大小的前缀(Header)

    在上一篇博文中,有介绍到用换行符分割消息的方法.但是这种方法有个小问题,如果消息中本身就包含换行符,那将会将这条消息分割成两条,结果就不对了. 本文介绍另外一种消息分割方式,即上一篇博文中讲的第2条: ...

  7. Mina、Netty、Twisted一起学(四):定制自己的协议

    在前面的博文中,介绍一些消息分割的方案,以及MINA.Netty.Twisted针对这些方案提供的相关API.例如MINA的TextLineCodecFactory.PrefixedStringCod ...

  8. Mina、Netty、Twisted一起学(二):TCP消息边界问题及按行分割消息

    在TCP连接开始到结束连接,之间可能会多次传输数据,也就是服务器和客户端之间可能会在连接过程中互相传输多条消息.理想状况是一方每发送一条消息,另一方就立即接收到一条,也就是一次write对应一次rea ...

  9. Mina、Netty、Twisted一起学(一):实现简单的TCP服务器

    MINA.Netty.Twisted为什么放在一起学习?首先,不妨先分别看一下它们官方网站对其的介绍: MINA: Apache MINA is a network application frame ...

随机推荐

  1. PYTHON学习之路_PYTHON基础(8)

    学习内容: Python模块介绍 1.经典类 or 新式类 2.抽象接口 3.静态方法.类方法.属性方法 4.反射 5.异常处理 6.socket编程初识 7.用socket实现get.put文件等功 ...

  2. 如何参与Linux内核开发(转)

    本文来源于linux内核代码的Document文件夹下的Hoto文件.Chinese translated version of Documentation/HOWTO If you have any ...

  3. Texture2D.GetPixelBilinear(float u, float v)的使用,官方例子注释

    using UnityEngine; using System.Collections; public class TEST : MonoBehaviour { public Texture2D so ...

  4. GCC 源码编译 mpc mprf gmp 不用make(否则会有lib/libgmp.so: could not read symbols: File in wrong format等错误)

    错误信息: lib/libgmp.so: could not read symbols: File in wrong formatcollect2: error: ld returned 1 exit ...

  5. python之raise、assert、with/as环境管理器

    要故意出发异常,可以使用raise语句,形式如下: raise <name>  #manually trigger an exception raise<name>,<v ...

  6. C++混合编程之idlcpp教程Python篇(7)

    上一篇在这 C++混合编程之idlcpp教程Python篇(6) 第一篇在这 C++混合编程之idlcpp教程(一) 与PythonTutorial4工程相似,工程PythonTutorial5中,同 ...

  7. 在PHP中使用Mysqli操作数据库

    PHP的 mysqli 扩展提供了其先行版本的所有功能,此外,由于 MySQL 已经是一个 具有完整特性的数据库服务器 , 这为PHP 又添加了一些新特性 . 而 mysqli 恰恰也支持了 这些新特 ...

  8. 【vscode】如何在vscode 中配置:TypeScript开发node环境

    入门流程,大神绕行. 安装环境 这就不多说了,安装开发的环境. 安装vscode 下载地址:https://code.visualstudio.com/ 安装Nodejs 下载地址:https://n ...

  9. nginx(1、正向代理)

    正向代理是指客户端通过代理服务器访问某一个服务器,最常见的例子是内网用户通过代理访问外网,以及所谓的“FQ”. 在windows下实例如下: 1.下载安装包:nginx-1.7.9.zip: 2.解压 ...

  10. Java IO7:管道流、对象流

    前言 前面的文章主要讲了文件字符输入流FileWriter.文件字符输出流FileReader.文件字节输出流FileOutputStream.文件字节输入流FileInputStream,这些都是常 ...