Netty使用websocket协议实现汽车行驶轨迹追踪demo

一、demo说明

本demo使用netty实现了在网页上实时查看汽车行驶当前位置的功能。当然这里只是一个演示的demo,所有的位置数据都是预先在百度地图上采集好保存在服务端的。代码参考了《Netty权威指南》。效果如下:



二、流程说明

当打开网页时,创建一个websocket连接到服务端。服务端用一个map来保存ChannelHandlerContext,key是ChannelHandlerContext,value为时间戳。同时开启一个线程,每500毫秒向浏览器发送经纬度数据。已打开的网页端每隔5秒钟向服务端发送一个心跳包,服务端收到后更新map中的时间戳。服务端每隔10秒钟就会去遍历这个map,将30秒内没有发送心跳包的ChannelHandlerContext剔除,同时终止发送经纬度数据的线程。


三、主要代码解析

CarTracerServer类比较简单,创建了两个NioEventLoopGroup实例。NioEventLoopGroup是个线程组,它包含了一组NIO线程,专门用于网络事件的处理,实际上它们就是Reactor线程组。这里创建两个的原因是一个用于服务端接收客户端的连接,另一个用于SocketChannel的网络读写。(《Netty权威指南》)。HttpServerCodec用于将请求和应答消息编码或者解码为HTTP消息;HttpObjectAggregator的目的是将HTTP消息的多个部分组合成一条完整的HTTP消息;ChunkedWriterHandler的目的是用来向客户端发送HTML5文件,它主要用于支持浏览器和服务端进行WebSocket通信。

package com.cartracer.my;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
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;

/**
 * 汽车行驶轨迹追踪服务端
 */
public class CarTracerServer {

	public void run(int port){
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workerGroup)
					.option(ChannelOption.SO_KEEPALIVE, true)
					.channel(NioServerSocketChannel.class)
					.childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						protected void initChannel(SocketChannel ch) throws Exception {
							ch.pipeline()
									.addLast("http-codec", new HttpServerCodec())
									.addLast("aggregator", new HttpObjectAggregator(65536))
									.addLast("http-chunked", new ChunkedWriteHandler())
									.addLast( new CarTracerServerHandler());
						}
					});
			Channel ch = b.bind(port).sync().channel();
			ch.closeFuture().sync();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}

	public static void main(String[] args) {
		new Thread(new AudienceListener()).start();
		new CarTracerServer().run(9999);
	}
}

CarTracerServerHandler如下。第一次握手请求的消息是HTTP消息,由handleHttpRequest方法处理,如果HTTP解码失败或者请求头中的Upgrade字段的值不是websocket则返回400响应。校验通过之后,通过握手处理类WebSocketServerHandshaker构造握手响应消息返回给客户端。

package com.cartracer.my;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
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.websocketx.*;
import io.netty.util.CharsetUtil;

import java.util.logging.Logger;

import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive;
import static io.netty.handler.codec.http.HttpHeaders.setContentLength;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

/**
 * CarTracerServerHandler
 */
public class CarTracerServerHandler extends SimpleChannelInboundHandler<Object> {

	//private static final String webSocketUrl = "ws://localhost:9999/websocket";

	private static final String webSocketUrl = "ws://carmove.zhutulang.cn:9999/websocket";

	private static final String HEART_BEAT_MSG = "I am alive";

    private static final Logger logger = Logger.getLogger(CarTracerServerHandler.class.getName());

    private WebSocketServerHandshaker handshaker = null;

	@Override
	public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
		AudienceListener.addAudience(ctx);
	}

	@Override
	public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
		logger.info("handler removed:"+ctx.hashCode());
		AudienceListener.removeAudience(ctx);
	}

	@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();
    }

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
		// 如果HTTP解码失败,返回HHTP异常
		if (!req.getDecoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
		    sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
		    return;
		}
		// 构造握手响应返回,本机测试
		WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(webSocketUrl, null, false);
		handshaker = wsFactory.newHandshaker(req);
		if (handshaker == null) {
		    WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
		} else {
		    handshaker.handshake(ctx.channel(), req);
		}
    }

    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
	    // 判断是否是关闭链路的指令
	    if (frame instanceof CloseWebSocketFrame) {
		    handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
		    return;
	    }

	    // 仅支持文本消息,不支持二进制消息
	    if (!(frame instanceof TextWebSocketFrame)) {
		    throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName()));
	    }

	    // 如果是心跳消息,更新时间戳
	    String clientSendMsg = ((TextWebSocketFrame) frame).text();
	    if (HEART_BEAT_MSG.equalsIgnoreCase(clientSendMsg)) {
		    AudienceListener.updateAudience(ctx);
	    }
    }

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

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

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		ctx.close();
    }
}

在以上代码中的AudienceListener持有两个ConcurrentHashMap,audiences和sendFutures。audiences用于保存连接上的ChannelHandlerContext,value是时间戳。当服务端接收到客户端发送来的心跳消息时,将更新这个时间戳,这个时间戳将会作为服务端判断客户端是否已经断开连接的主要依据。sendFutures用于保存提交给线程池返回的Future,每当一个websocket连接建立后,服务端将会开启一个线程向客户端循环发送经纬度数据。这个线程代码如下:

package com.cartracer.my;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;

/**
 * 发送位置数据线程
 */
public class LocationMsgSender implements Runnable {

	private ChannelHandlerContext ctx;

	private static final Logger logger = Logger.getLogger(LocationMsgSender.class.getName());

	public LocationMsgSender(ChannelHandlerContext ctx){
		this.ctx = ctx;
	}

	// 预先保存好的汽车行驶轨迹经纬度数据
	List<String> data = Arrays.asList(new String[]{
			"113.980665,22.543174","113.980953,22.543108","113.981096,22.542907","113.98124,22.542841","113.981528,22.542841","113.981815,22.54264",
			"113.981959,22.542574","113.982174,22.542373","113.98239,22.542373","113.982462,22.542306","113.982821,22.542306","113.983252,22.542039",
			"113.983396,22.542039","113.98354,22.542039","113.98354,22.541906","113.983827,22.541772","113.983971,22.541772","113.984115,22.541772",
			"113.984187,22.541772","113.984402,22.541505","113.984546,22.541505","113.98469,22.541505","113.984833,22.541505","113.985121,22.541238",
			"113.985265,22.541238","113.985839,22.541038","113.985983,22.540971","113.986271,22.540971","113.986414,22.540838","113.986702,22.540704",
			"113.986774,22.540704","113.987133,22.540504","113.987133,22.540437","113.987277,22.540437","113.98742,22.540437","113.987852,22.540437",
			"113.987924,22.540304","113.988211,22.540237","113.988283,22.54017","113.98857,22.540037","113.988714,22.53997","113.989001,22.53997",
			"113.989073,22.539903","113.989145,22.539903","113.989433,22.539903","113.989576,22.539903","113.98972,22.539903","113.989936,22.539903",
			"113.990151,22.53977","113.990582,22.53977","113.990726,22.53977","113.99087,22.53977","113.991157,22.539903","113.991301,22.539903",
			"113.991589,22.539903","113.991732,22.539903","113.991876,22.539903","113.99202,22.539903","113.992451,22.539903","113.992738,22.539903",
			"113.993026,22.539903","113.993098,22.539903","113.993313,22.539903","113.993457,22.539903","113.993673,22.539903","113.99396,22.539903",
			"113.994176,22.539903","113.994607,22.539903","113.994894,22.539903","113.99511,22.539903","113.995182,22.539903","113.995326,22.539903",
			"113.995613,22.539903","113.995757,22.539903","113.995972,22.53977","113.996044,22.53977","113.996332,22.53977","113.996475,22.53977",
			"113.996835,22.53977","113.997122,22.53977","113.997481,22.53977","113.997913,22.53977","113.998056,22.53977","113.9982,22.53977",
			"113.998488,22.53977","113.998775,22.53977","113.998919,22.53977","113.999062,22.53977","113.999206,22.539703","113.99935,22.539636",
			"113.999494,22.539636","113.999709,22.539636","114.000069,22.539636","114.000212,22.539503","114.0005,22.539503","114.000859,22.539369",
			"114.001147,22.539369","114.001218,22.539369","114.001434,22.539235","114.001506,22.539235","114.001721,22.539169","114.001937,22.539102",
			"114.002296,22.539102","114.002584,22.538968","114.002799,22.538968","114.003087,22.538902","114.003374,22.538835","114.003662,22.538835",
			"114.004093,22.538835","114.004524,22.538635","114.004668,22.538635","114.005171,22.538568","114.005674,22.538568","114.005818,22.538568",
			"114.006105,22.538568","114.006824,22.538568","114.007111,22.538434","114.007399,22.538434","114.007686,22.538434","114.008117,22.538434",
			"114.008261,22.538434","114.008549,22.538434","114.008836,22.538434","114.00898,22.538434","114.009124,22.538434","114.009483,22.538434",
			"114.009842,22.538434","114.01013,22.538568","114.010417,22.538568","114.010848,22.538568","114.011279,22.538568","114.011567,22.538568",
			"114.011782,22.538568","114.012142,22.538635","114.012286,22.538635","114.012573,22.538701","114.01286,22.538835","114.013004,22.538835",
			"114.013292,22.538835","114.013579,22.538835","114.013867,22.538835","114.014298,22.538835","114.014585,22.538902","114.014729,22.538902",
			"114.01516,22.538968","114.015232,22.539102","114.015448,22.539102","114.015735,22.539102","114.015879,22.539102","114.016094,22.539169",
			"114.01631,22.539169","114.016597,22.539102","114.016741,22.539102","114.016885,22.539102","114.016957,22.539169","114.017747,22.539169",
			"114.017819,22.539169","114.018035,22.539169","114.018107,22.539369","114.018394,22.539369","114.018466,22.539369","114.018753,22.539436",
			"114.018969,22.539436","114.019185,22.539436","114.019544,22.539503","114.019903,22.539636","114.020191,22.539636","114.020622,22.539636",

	});

	int i = 0;
	@Override
	public void run() {
		while(true && !Thread.currentThread().isInterrupted()){
			if(i == data.size()){
				i = 0;
			}
			logger.info(ctx.hashCode()+", data send="+data.get(i));
			ctx.channel().write(new TextWebSocketFrame(data.get(i)));
			ctx.flush();
			i++;
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
				Thread.currentThread().interrupt();
			}
		}
	}
}

AudienceListener本身也是一个线程,定时检查audiences中的ChannelHandlerContext的时间戳和现在时间是否超过30秒,如果超过则说明该客户端已经下线,将其剔除。

package com.cartracer.my;

import io.netty.channel.ChannelHandlerContext;

import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Logger;

/**
 * AudienceListner
 */
public class AudienceListener implements Runnable{

	private static ExecutorService exe = Executors.newFixedThreadPool(10);

	public static ConcurrentHashMap<ChannelHandlerContext, Long> audiences = new ConcurrentHashMap<>();

	public static ConcurrentHashMap<ChannelHandlerContext, Future> sendFutures = new ConcurrentHashMap<>();

	private static final Logger logger = Logger.getLogger(LocationMsgSender.class.getName());

	public static void addAudience(ChannelHandlerContext ctx){
		logger.info("新入channel:"+ctx.hashCode());
		audiences.putIfAbsent(ctx, System.currentTimeMillis());
		Future f = exe.submit(new LocationMsgSender(ctx));
		sendFutures.putIfAbsent(ctx, f);
		logger.info("audience大小:"+audiences.size());
	}

	public static void removeAudience(ChannelHandlerContext ctx){
		logger.info("移除channel:"+ctx.hashCode());
		audiences.remove(ctx);
		// 终止该channel LocationMsgSender 线程
		Future f = sendFutures.get(ctx);
		if(f != null){
			f.cancel(true);
		}
		sendFutures.remove(ctx);
	}

	public static void updateAudience(ChannelHandlerContext ctx){
		logger.info("channel 接收到心跳包,更新时间戳");
		audiences.put(ctx, System.currentTimeMillis());
		logger.info("audience大小:"+audiences.size());
	}

	@Override
	public void run() {
		try {
			while(true){
				Thread.sleep(10000);
				Iterator<Map.Entry<ChannelHandlerContext, Long>> itr = audiences.entrySet().iterator();
				long currentTime = System.currentTimeMillis();
				while(itr.hasNext()){
					Map.Entry<ChannelHandlerContext, Long> entry = itr.next();
					if(currentTime - entry.getValue() >= 30000){
						// 表明该channel已经断开连接
						removeAudience(entry.getKey());
					}
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

网页端代码如下,比较简单,在onmessage方法里实时在百度地图上标记。注意使用百度地图需要注册,引入你自己的key, <script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak= yourkey"></script> 

<!DOCTYPE html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
	<style type="text/css">
		body, html{width: 100%;height: 100%;margin:0;font-family:"微软雅黑";}
		#allmap{height:500px;width:100%;}
		#r-result{width:100%; font-size:50px;font-weight:bold;text-align:center;font-family:微软雅黑;}
	</style>
	<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=yourkey"></script>
	<title>Car Tracer</title>
</head>
<body>
	<div id="allmap"></div>
	<div id="r-result">
		Car Tracer
	</div>
</body>
</html>
<script type="text/javascript">
	// 百度地图API功能
	var map = new BMap.Map("allmap");
	map.centerAndZoom(new BMap.Point(113.980665,22.543174),15);
	map.enableScrollWheelZoom(true);

	//标记当前位置
	function markLocation(msg){
		var lh = msg.split(",");
		map.clearOverlays();
		var new_point = new BMap.Point(lh[0],lh[1]);
		var marker = new BMap.Marker(new_point);  // 创建标注
		map.addOverlay(marker);              // 将标注添加到地图中
		map.panTo(new_point);
	}
</script>
<script type="text/javascript">
	var socket;
	if (!window.WebSocket)
	{
		window.WebSocket = window.MozWebSocket;
	}
	if (window.WebSocket) {
		socket = new WebSocket("ws://localhost:9999/websocket");
		socket.onmessage = function(event) {
			markLocation(event.data);
		};
		socket.onopen = function(event) {
		};
		socket.onclose = function(event) {
		};
	} else {
		alert("抱歉,您的浏览器不支持WebSocket协议!");
	}

	//发送心跳包
	window.setInterval(function(){
		if(socket.readyState == WebSocket.OPEN){
			socket.send("I am alive");
		}
	},5000);
</script>


其它细节不再赘述,传送门,github地址: https://github.com/zhutulang/cartracer/tree/master

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值