准备
JDK 7+
Maven 3.2.x
Netty 5.x
Eclipse 4.x
我的使用场景包括用户的登录、注销、获取通讯录和发送消息,对于应用场景复杂的应该要考虑更多的情况,根据我在工作中的经验,通讯协议不是一下就设计好的,而是在开发过程进行不断修改与完善,可以说没有协议的设计只能遵循具体的原则,没有最终版。 我在工作中原本是基于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 { private static final Logger LOG = Logger.getLogger(BananaWebSocketServerHandler.class.getName()); // map用于channel和具体的用户名绑定起来,可以根据具体业务实现认证信息和channel绑定 static final Map channelMap = Collections.synchronizedMap(new HashMap()); // set保存登陆的用户信息 static final List set = new ArrayList(); 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握手 * 注意: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() { @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