浅谈Session及Netty实现

73718346_p0.png

1. 浅谈Session

1.1 什么是Session

session称之为会话,可以看做是客户端和服务器对话的介质。通过session赋予每个客户端的sessionId,服务端可以分辨各个不同的客户端。

TIM截图20190319222114.png

所以说,session实际上时一种会话机制,具体来说在服务端是一种数据结构,而它的运行依赖于sessionId;而sessionId需要存储到客户端的cookie当中,并且在每次请求提交时都将cookie置于请求头当中。

这样,服务器就能从请求头中获取到sessionId,并通过该ID获取到session记录(由服务端来维护,一般存储到内存某数据结构当中),从而分辨出相应的客户端。

1.2 与cookie区别

通过上面的几段话可以很明白的看出:

  • session在服务器端,而cookie在客户端(例如浏览器);
  • session的运行依赖 sessionId,而 sessionId 是存在 cookie中的;
  • 维持一个会话的核心就是客户端的唯一标识,即 sessionId

2. Netty 实现

2.1 维护Session

Netty中默认不提供session的实现以及对应的POJO或者接口类,我们需要自己来定义HttpSession类,用于存储一些数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class HttpSession {

private String id;

private Map<String, Object> attributes = new HashMap<>();

public HttpSession(String id) {
this.id = id;
}

public Object getAttribute(String name) {
return attributes.get(name);
}

public void addAttribute(String name, Object value) {
attributes.put(name, value);
}

public String getId() {
return id;
}

}

之后,我们需要建立session idHttpSession的映射关系,我们通过一个session管理类SessionManager来维护这种映射关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class SessionManager {

private static final HashMap<String, HttpSession> sessionMap = new HashMap<>();

/**
* 注册Session,并返回新注册的HttpSession对象
* @return
*/
public static HttpSession addSession() {
String sessionId = getSessionId();
synchronized (sessionMap) {
HttpSession session = new HttpSession(sessionId);
sessionMap.put(sessionId, session);
return session;
}
}

/**
* 判断当前服务端是否有该 session id 的记录
*/
public static boolean containsSession(String sessionId){
synchronized (sessionMap) {
return sessionMap.containsKey(sessionId);
}
}

public static HttpSession getSession(String sessionId) {
return sessionMap.get(sessionId);
}

private static String getSessionId() {
return UUID.randomUUID().toString().replace("-", "");
}

}

2.2 Http处理器

Netty为cookie提供了抽象的接口类Cookie,这使得我们可以很简单地操作Cookie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 注意的是该包路径,io.netty.handler.codec.http下的Cookie接口已经被弃用
package io.netty.handler.codec.http.cookie;

public interface Cookie extends Comparable<Cookie> {

String name();
String value();
String path();
long maxAge();

void setValue(String value);
void setPath(String path);
void setMaxAge(long maxAge);

...
}

同时还为Cookiecookie字符串提供编解码器:

  • ServerCookieEncoder:编码器,将Cookie对象编码为cookie字符串;
  • ServerCookieDecoder:解码器,将cookie字符串解码为Cookie对象

举个例子:

1
2
3
4
5
// ServerCookieDecoder
JSESSIONID=123 -> Cookie{key=JSESSIONID, value="123"}

// ServerCookieEncoder
Cookie{key=JSESSIONID, value="456"} -> JSESSIONID=456

有了cookie的抽象接口和与字符串的编解码器,我们就能很容易的操作数据。具体的操作流程在HttpRequestHandler接口请求处理的channelRead0操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

private static final String CLIENT_COOKIE_NAME = "JSESSIONID";

protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
// 构造 FullHttpResponse 对象
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1 ,
HttpResponseStatus.OK,
Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8));

// 如果该客户端该服务端中不存在 Session记录,将会注册新的Session
// 并通过 set-cookie 的响应头让客户端保存的sessionId
if (!hasSessionId(request)) {
HttpSession session = SessionManager.addSession();
// 创建 Cookie 对象,并通过 ServerCookieEncoder 编码为cookie字符串
Cookie cookie = new DefaultCookie(CLIENT_COOKIE_NAME, session.getId());
cookie.setPath("/");
String cookieStr = ServerCookieEncoder.STRICT.encode(cookie);
response.headers().set(HttpHeaderNames.SET_COOKIE, cookieStr);
}
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}

/**
* 判断该客户端在服务端中是否有 Session 记录
*/
private boolean hasSessionId(FullHttpRequest request) {
// 从客户端请求头中得到cookie字符串
String cookieStr = request.headers().get("Cookie");
if (cookieStr != null) {
// 通过 ServerCookieDecoder 将cookie字符串解码为 Cookie 对象
Set<Cookie> cookies = ServerCookieDecoder.STRICT.decode(cookieStr);
for (Cookie cookie : cookies) {
if (cookie.name().equals(CLIENT_COOKIE_NAME) &&
SessionManager.containsSession(cookie.value())) {
HttpSession session = SessionManager.getSession(cookie.value());
System.out.println(session);
return true;
}
}
}
return false;
}

}

需要注意的是,在判断该客户端在服务端中是否有 Session记录时,需要保证两个条件:

  • cookie中具有键为JSESSIONID的值;
  • 服务器中(即SessionManager中)需要有sessionId对应的session

默认情况下,客户端的cookie为空:

TIM截图20190320113935.png

当向服务器发起一次情况之后,客户端将保存服务器响应头中设置的session id

TIM截图20190320114000.png

这样,我们就很简单地实现了session功能。如果作为一个合格的框架,还需要创建自己的HTTPRequest对象,而不是直接将FullHttpRequest暴露给客户,并且需要将HttpSession注入其中:

1
2
3
4
5
6
7
8
9
10
public class HttpRequest {

// 其他的一些属性...

private HttpSession session;

public HttpSession getSession() {
return session;
}
}

如果你感兴趣,可以参考我写的schla-webmvc框架,Github地址为:https://github.com/pushyzheng/schla-webmvc

本文demo见Github

Pushy wechat
欢迎订阅我的微信公众号