jiaji's blog

长连接通道解决方案

为什么要长连接

资源中心or控制中心

很久以来,服务端一直作为“统一资源中心”而存在,所以后台开发工程师们的基础工作就是存储、维护和管理用户的数据,然后开接口给客户端&&前端同学使用。后来出现了restful架构风格:一个URI代表一个资源,客户端用HTTP协议的操作方式(GET、POST、PUT、DELETE)对资源进行操作,即完成了与服务端的交互。这样的设计可以让服务端的逻辑变得非常的轻,真的变成了“资源中心”了。客户端可以依托操作资源的接口去实现真正的业务逻辑,渲染界面。考虑到一些异常情况处理,逻辑会做的比较重。

对于后台开发同学来说,一定不会满足于“资源管理者”这样的角色,每天写写增删改查和字符串拼接。因为有长连接技术的存在,服务端可以做更多的事情。相对于客户端把服务端当成“数据资源库”,反过来,服务端也可以把客户端看成“UI库”,通过长连接通道推送命令下去,指导客户端UI进行变化。这样服务端就从“资源中心”变成了“控制中心”。当然这是一种比较极端的设计,长连接可能因为网络或者客户端环境问题断掉,这样控制中心就失去控制能力了。所以在实现业务时可以用推拉结合的模式。

即时触达的能力

因为有了长连接通道,服务端有了消息即时触达的能力,同时避免了客户端分布式攻击式的轮询。这个能力是非常非常重要的,在设计在线网络游戏、IM系统、推送系统时长连接通道不可或缺。

业界通用推送方法

现在移动端都有自己的push系统,iOS有APNS(Apple Push Notification service),Android有GCM(Google Cloud Messaging)。可以简单理解为官方的一个服务和移动客户端建立了一个长连接,所有的app共用这条通道,每次推送时要用自己的服务调用一下官方服务的接口,然后由官方的服务进行推送,客户端收到推送后会在系统通知栏看到app推送的一个概要信息,用户点击这条push,会唤起对应的app,进入app后会调用app自己的服务接口拉取更多的业务数据。

因为哈哈哈哈的原因,GCM在国内几乎不能使用;如果应用有pc端还要支持pc端的推送;业务比较复杂时会希望设计一种更高效灵活的推送协议。基于以上原因,开发者会设计自己的长连接通道和推送协议。

客户端长连接方案

下边给大家介绍一款轻量级的长连接&&推送框架(依赖netty4.1.2) rivendell,名字出自《the Lord of the Rings》。

框架特性

  • 长连接管理

map存储channel id和ChannelHandlerContext。方便对在线用户进行管理。

  • 业务异步化

默认情况下,netty是有1个boss线程来accept连接,然后分给availableProcessors*2个worker线程进行处理。为个提高并发量,这几个worker线程是不能去做耗时的业务操作的。在这里worker线程只做编解码,然后根据协议的action,分发到线程池中异步执行相应任务。

  • 断线重连

长连接的保活是非常重要的工作,在这里的实现是客户端监听IdleStateEvent事件,每10s发送心跳包,服务端收到心跳包后会echo给客户端;当客户端25s没有收到读消息则触发断线重连。

  • 扩展性

定义了简单的协议SimpleProtocol:

1
2
3
private boolean success;(本次动作是否成功)
private String action;(动作类型)
private String content;(动作内容)

可以基于此协议实现自己的业务,需要实现IActionHandler接口,当然也可以定义自己的协议。

  • 监控&&推送后台

可以看到实时长连接数,并提供了推送入口。支持单点推送和广播。

代码路径

服务端相关代码在com.fantasy.rivendell.service.server包中,客户端相关代码在com.fantasy.rivendell.service.client包中,控制台的入口类是RivendellController。如果有问题欢迎找我讨论😆。

浏览器长连接方案

浏览器实现长连接,自然想到了websocket。这里就不介绍websocket是如何交换sec-key如何upgrade协议了,只讲下具体实现方案:

原生实现

依赖tomcat容器7.0.47以上版本,并且需要引入包:

1
2
3
4
5
6
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>

在Websocket处理类上加注解@ServerEndpoint(“/websocket路径”)后,监听onOpen、onMessage、onClose时间即可。每次一个新的会话都会创建一个新的本类的实例。比较尴尬的是,这种方式和Spring是不兼容的,不能同时使用Spring进行bean的管理。

spring中的实现

首先引入spring websocket相关的包:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>

继承WebSocketConfigurer类,配置websocket入口:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(renjuHandler(), "/入口路径");
}
@Bean
public WebSocketHandler bizHandler() {
return new BizHandler();
}
}

业务逻辑写在BizHandler中,BizHandler可以继承TextWebSocketHandler,重载父类处理函数。注意,这里的BizHandler是一个被spring管理的bean,这样就可以愉快的进行注入了。

浏览器端直接用html5中的Websocket就行:

1
2
3
4
5
6
7
var connection = new WebSocket("ws://127.0.0.1:8080/入口路径");
connection.open = function(){
do some biz...
}
connection.onmessage= function(e){
do some biz...
}

最后附上一个使用websocket实现的在线五子棋游戏demo:renju

思考和总结

分布式环境

单台服务器可打开的最大连接数是有限的,当用户量比较大时需要提供一个集群来给用户建立长连接。同时还需要一个位置服务:用户来连接时经过一致性hash计算落到某台机器上,记录在位置服务中;要推送时要首先查询位置服务,拿到用户此时连在哪台机器,之后向这个机器投递消息,进行推送。

长连接安全问题

攻击者和长连接服务建立tcp连接,这一步是没有办法校验的,所以对于syn flood攻击需要其他机制来防御。建立连接后可以用非对称加密来交换对称加密的秘钥,之后经过对称秘钥加密解密进行传输。对于服务端来说,如果收到无法识别,或者解密失败的消息,直接丢弃。

长连接作为代理

通过长连接代理请求后端服务,可以避免创建socket和三次握手的消耗,提高性能。

长连接保活机制

长连接如果不存在了有两种表现:1是再使用这个通道时会收到异常;2是发送的心跳包超过一定时间没有响应。所以判断长连接是否存在需要有具体的机制来触发监测,看是否会出现这两种表现。本文中rivendell项目实现的是由客户端主动发起心跳监测,服务端收到心跳校测后echo给客户端,客户端根据拿到echo后重置IdleState。服务端要有一个时间轮来管理所有连接,及时清理掉不存在的连接的上下文。

当客户端判断连接真的不存在时,要有一定的退避机制,不能立刻重连。

websocket和http2

http2的特性中有一条是支持server push,那么http2是否可以取代websocket呢?答案是否定的。http2为了提高性能也有一个持久的连接,它鼓励服务端尽量长久的维持这个连接,但是也允许服务端在必要时刻关掉idle的连接。所以http2并不能取代websocket,它们可以互相补充。

有任何问题,欢迎留言或者邮件交流~