使用Netty框架开发websocket即时通讯

准备

  • JDK 7+
  • Maven 3.2.x
  • Netty 5.x
  • Eclipse 4.x

<!– more –>


我的使用场景包括用户的登录、注销、获取通讯录和发送消息,对于应用场景复杂的应该要考虑更多的情况,根据我在工作中的经验,通讯协议不是一下就设计好的,而是在开发过程进行不断修改与完善,可以说没有协议的设计只能遵循具体的原则,没有最终版。
我在工作中原本是基于XMPP开发的,由于做的是移动互联的应用,受限于移动网络的网速,而XMPP的协议过于庞大,对用户的流量需求太高,为此我开始寻找XMPP的替代品,我最开始选的方案是Google的ProtoBuf,类似的还有Apache的Thrift,这两者都是二进制级别的编码,虽然两者的压缩程度和性能都非常好,但在通讯协议方面不太适合,因为编码后没有可读性,出了问题不好定位。后来我在工作选了JSON来设计,相比XML来说拓展性与性能都要好很多。

3、客户端程序设计
客户端采用的是Flex,使用的集成开发工具是IntelliJ IDEA,基于Apache Flex SDK
根据通讯协议的设计进行客户端的开发,具体代码如下:

websocket连接建立前,客户端需要与服务器进行握手(http协议) 确认websocket连接,也就是说在处理websocket请求前,必需要处理一些http请求

1.Lanucher

package com.company.lanucher;


import com.company.server.WebSocketServer;


public class Lanucher {


public static void main(String[] args) throws Exception {
// 启动WebSocket
new WebSocketServer().run(WebSocketServer.PORT);
}

}

2.BananaWebSocketServerHandler

package com.company.server;


import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


import org.apache.log4j.Logger;


import com.company.serviceimpl.BananaService;
import com.company.util.CODE;
import com.company.util.Request;
import com.company.util.Response;
import com.google.common.base.Strings;
import com.google.gson.JsonSyntaxException;


import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.util.CharsetUtil;




/**
 * WebSocket服务端Handler
 *
 */
public class BananaWebSocketServerHandler extends SimpleChannelInboundHandler<Object> {

private static final Logger LOG = Logger.getLogger(BananaWebSocketServerHandler.class.getName());
// map用于channel和具体的用户名绑定起来,可以根据具体业务实现认证信息和channel绑定
    static final Map<String ,Channel> channelMap = Collections.synchronizedMap(new HashMap<String ,Channel>());
    // set保存登陆的用户信息
static final List<String> set = new ArrayList<String>();
private WebSocketServerHandshaker handshaker;
private ChannelHandlerContext ctx;
private String sessionId;


@Override
public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
// 传统的HTTP接入
if (msg instanceof FullHttpRequest) { 
handleHttpRequest(ctx, (FullHttpRequest) msg);
// WebSocket接入
} else if (msg instanceof WebSocketFrame) { 
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}


@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
LOG.error("WebSocket异常", cause);
ctx.close();
LOG.info(sessionId + " 注销");
BananaService.logout(sessionId); // 注销
BananaService.notifyDownline(sessionId); // 通知有人下线
}
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
LOG.info("WebSocket关闭");
super.close(ctx, promise);
LOG.info(sessionId + " 注销");
BananaService.logout(sessionId); // 注销
BananaService.notifyDownline(sessionId); // 通知有人下线
}


/**
* 处理Http请求,完成WebSocket握手<br/>
* 注意:WebSocket连接第一次请求使用的是Http
* @param ctx
* @param request
* @throws Exception
*/
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
// 如果HTTP解码失败,返回HHTP异常
if (!request.getDecoderResult().isSuccess() || (!"websocket".equals(request.headers().get("Upgrade")))) {
sendHttpResponse(ctx, request, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}


// 正常WebSocket的Http连接请求,构造握手响应返回
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://" + request.headers().get(HttpHeaders.Names.HOST), null, false);
handshaker = wsFactory.newHandshaker(request);
if (handshaker == null) { // 无法处理的websocket版本
WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
} else { // 向客户端发送websocket握手,完成握手
handshaker.handshake(ctx.channel(), request);
// 记录管道处理上下文,便于服务器推送数据到客户端
this.ctx = ctx;
}
}


/**
* 处理Socket请求
* @param ctx
* @param frame
* @throws Exception 
*/
private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
// 判断是否是关闭链路的指令
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
// 判断是否是Ping消息
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
return;
}
// 当前只支持文本消息,不支持二进制消息
if (!(frame instanceof TextWebSocketFrame)) {
throw new UnsupportedOperationException("当前只支持文本消息,不支持二进制消息");
}

// 处理来自客户端的WebSocket请求
try {
Request request = Request.create(((TextWebSocketFrame)frame).text());
Response response = new Response();
response.setServiceId(request.getServiceId());
if (CODE.online.code.intValue() == request.getServiceId()) { // 客户端注册
String requestId = request.getRequestId();
if (Strings.isNullOrEmpty(requestId)) {
response.setIsSucc(false).setMessage("requestId不能为空");
return;
} else if (Strings.isNullOrEmpty(request.getName())) {
response.setIsSucc(false).setMessage("name不能为空");
return;
} else if (BananaService.bananaWatchMap.containsKey(requestId)) {
response.setIsSucc(false).setMessage("您已经注册了,不能重复注册");
return;
}
if (!BananaService.register(requestId, new BananaService(ctx, request.getName()))) {
response.setIsSucc(false).setMessage("注册失败");
} else {
response.setIsSucc(true).setMessage("注册成功");




//channelMap.put(request.getRequestId(), ctx.channel());
//set.add(request.getRequestId());

BananaService.bananaWatchMap.forEach((reqId, callBack) -> {
response.getHadOnline().put(reqId, ((BananaService)callBack).getName()); // 将已经上线的人员返回
//添加用户列表到集合中
/*UserBean userBean=new UserBean();
userBean.setUserId(reqId);
userBean.setUserName(((BananaService)callBack).getName());
response.getUsers().add(userBean);*/

if (!reqId.equals(requestId)) {
Request serviceRequest = new Request();
serviceRequest.setServiceId(CODE.online.code);
serviceRequest.setRequestId(requestId);
serviceRequest.setName(request.getName());
//serviceRequest.set
try {
callBack.send(serviceRequest); // 通知有人上线
} catch (Exception e) {
LOG.warn("回调发送消息给客户端异常", e);
}
}
});
}
System.out.println("response(login)========="+response.toJson());
sendWebSocket(response.toJson());
this.sessionId = requestId; // 记录会话id,当页面刷新或浏览器关闭时,注销掉此链路
} else if (CODE.send_message.code.intValue() == request.getServiceId()) { // 客户端发送消息到聊天群
String requestId = request.getRequestId();
if (Strings.isNullOrEmpty(requestId)) {
response.setIsSucc(false).setMessage("requestId不能为空");
} else if (Strings.isNullOrEmpty(request.getName())) {
response.setIsSucc(false).setMessage("name不能为空");
} else if (Strings.isNullOrEmpty(request.getMessage())) {
response.setIsSucc(false).setMessage("message不能为空");
} else {
//response.setIsSucc(true).setMessage("发送消息成功");
if (request.getType()==1) {
BananaService.bananaWatchMap.forEach((reqId, callBack) -> { // 单聊
Request serviceRequest = new Request();
serviceRequest.setServiceId(CODE.receive_message.code);
serviceRequest.setRequestId(requestId);
serviceRequest.setName(request.getName());
serviceRequest.setMessage(request.getMessage());
serviceRequest.setTo(request.getTo());
serviceRequest.setType(1);
try {
callBack.send(serviceRequest);
} catch (Exception e) {
LOG.warn("回调发送消息给客户端异常", e);
}
});
sendWebSocket(response.toJson());

}else if(request.getType()==2) { //群聊
BananaService.bananaWatchMap.forEach((reqId, callBack) -> { // 将消息发送到所有机器
Request serviceRequest = new Request();
serviceRequest.setServiceId(CODE.receive_message.code);
serviceRequest.setRequestId(requestId);
serviceRequest.setName(request.getName());
serviceRequest.setMessage(request.getMessage());

try {
callBack.send(serviceRequest);
} catch (Exception e) {
LOG.warn("回调发送消息给客户端异常", e);
}
});


sendWebSocket(response.toJson());
}
}
} else if (CODE.downline.code.intValue() == request.getServiceId()) { // 客户端下线
String requestId = request.getRequestId();
if (Strings.isNullOrEmpty(requestId)) {
sendWebSocket(response.setIsSucc(false).setMessage("requestId不能为空").toJson());
} else {
BananaService.logout(requestId);
response.setIsSucc(true).setMessage("下线成功");
BananaService.notifyDownline(requestId); // 通知有人下线
sendWebSocket(response.toJson());
}

} else {
sendWebSocket(response.setIsSucc(false).setMessage("未知请求").toJson());
}
} catch (JsonSyntaxException e1) {
LOG.warn("Json解析异常", e1);
} catch (Exception e2) {
LOG.error("处理Socket请求异常", e2);
}
}


/**
* Http返回
* @param ctx
* @param request
* @param response
*/
private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {
// 返回应答给客户端
if (response.getStatus().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(response.getStatus().toString(), CharsetUtil.UTF_8);
response.content().writeBytes(buf);
buf.release();
HttpHeaders.setContentLength(response, response.content().readableBytes());
}


// 如果是非Keep-Alive,关闭连接
ChannelFuture f = ctx.channel().writeAndFlush(response);
if (!HttpHeaders.isKeepAlive(request) || response.getStatus().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}

/**
* WebSocket返回
* @param ctx
* @param req
* @param res
*/
public void sendWebSocket(String msg) throws Exception {
if (this.handshaker == null || this.ctx == null || this.ctx.isRemoved()) {
throw new Exception("尚未握手成功,无法向客户端发送WebSocket消息");
}
this.ctx.channel().write(new TextWebSocketFrame(msg));
this.ctx.flush();
}
}

3.WebSocketServer

package com.company.server;


import org.apache.log4j.Logger;


import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;


/**
 * WebSocket服务
 *
 */
public class WebSocketServer {
private static final Logger LOG = Logger.getLogger(WebSocketServer.class);

// websocket端口
public static final int PORT = 9090;


public void run(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<Channel>() {


@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast("http-codec", new HttpServerCodec()); // Http消息编码解码
pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); // Http消息组装
pipeline.addLast("http-chunked", new ChunkedWriteHandler()); // WebSocket通信支持
pipeline.addLast("handler", new BananaWebSocketServerHandler()); // WebSocket服务端Handler
}
});

Channel channel = b.bind(port).sync().channel();
LOG.info("WebSocket 已经启动,端口:" + port + ".");
channel.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}

}

话不多说需要的直接下载:http://download.csdn.net/detail/qq_36168479/9852856

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值