Mina框架Socket技术实现内外网通信

socket基本原理参考:http://blog.csdn.net/hudashi/article/details/50790002
java socket点对点通信参考: http://baike.xsoftlab.net/view/71.html
mina框架参考:http://blog.csdn.net/ljx8928358/article/details/7759024

项目需求:外网内网数据交互(外网app客户端发送请求到内网客户端,抓取需求数据)。

首先需要考虑的问题:

  1. 服务器和客户端的连接保持
    通过心跳机制定时监测连接是否断开。

  2. 客户端区分
    每个客户端设置唯一标识,服务器端需要保存客户端的映射关系

  3. 数据包的封装
    ①数据包标识
    ②请求参数:token,url,业务参数等等。
     考虑安全性使用加密。

    ③客户端唯一标识。

  4. 是否有文件传输

  5. 服务的开销(并发量的问题,因Socket的机制是异步处理)

设计流程图:

这里写图片描述

注意:Socket服务只负责转发数据,不参与具体的业务逻辑。

代码编写:

Socket Server

Client就不写了,比服务端多了一个超时重连的处理

@Value("${PORT}")
private int PORT; //配置文件读取Socket服务器端口
//创建IoAcceptor实例
IoAcceptor acceptor = new NioSocketAcceptor();
//绑定端口监听
acceptor.bind(new InetSocketAddress(PORT));  

2. 日志,文本拦截设置

//用来记录日志和打印事件消息
acceptor.getFilterChain().addLast("logger", new LoggingFilter());
//设置文本传输协议
TextLineCodecFactory textLineCodecFactory = new TextLineCodecFactory(Charset.forName("UTF-8"));
//设置传输大小
textLineCodecFactory.setDecoderMaxLineLength(1024*1024); //1M  
textLineCodecFactory.setEncoderMaxLineLength(1024*1024); //1M
acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(textLineCodecFactory));

3. 核心消息处理器IoHandler

写一个类根据需求重写IoHandler的方法。
注意Socket客户端(IoSession)断开连接的处理

@Override
public void sessionClosed(IoSession session) throws Exception {
	logger.info("关闭当前session:{}#{}", session.getId(), session.getRemoteAddress());
	Long sessionId = session.getId();
	CloseFuture closeFuture = session.close(true);
	closeFuture.addListener(new IoFutureListener<IoFuture>() {
		public void operationComplete(IoFuture future) {
			if (future instanceof CloseFuture) {
				((CloseFuture) future).setClosed();
				logger.info("sessionClosed CloseFuture setClosed-->{},", future.getSession().getId());
			}
		}
	});
	//session容器中移除对应的客户端session
	InitUtil.SessionMap.remove(sessionId);
	logger.info("客户端:"+sessionId+"关闭服务端缓存会话数:"+InitUtil.SessionMap.size());
}

@Override
	public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
		logger.info("服务器发生异常: {}", cause.getMessage());
		Long sessionId = session.getId();
		session.close(true);
		InitUtil.SessionMap.remove(sessionId);
	}

数据处理重写:

@Override
public void messageReceived(IoSession session, Object message) throws Exception {
	try {
		logger.info("服务器接收到数据: {}", message);
		SocketBean sb = JsonUtil.getObject(message.toString(), SocketBean.class);
		//如果该session第一次发送数据,映射Map中新建一个该客户端的对象
		if(null!=sb.firstSent && sb.firstSent){
			//存储APP端获取session对象映射map
			InitUtil.GetMap.put(sb.clientBusinessId, session.getId());
			logger.info("服务端缓存客户端详情clientBusinessMap:"+JsonUtil.toJsonString(InitUtil.GetMap));
			
		}else{
			UUID uid = sb.getUid();
			InitUtil.DataMap.put(uid, sb);
		}
	} catch (Exception e) {
		logger.error("服务器端接受客户端的数据异常:"+e);
	}
}

然后把IoHandler注册到acceptor中:

// 设置核心消息业务处理器
acceptor.setHandler(new MyServerHandler());

4. 心跳机制

设置心跳包内容以用来验证,实现KeepAliveMessageFactory接口

public class KeepAliveMessageFactoryImpl implements KeepAliveMessageFactory {
	private final Logger LOG = LoggerFactory.getLogger(KeepAliveMessageFactoryImpl.class);
	
	/** 心跳包内容 */
	private static final String HEARTBEATREQUEST = "0x11";
	private static final String HEARTBEATRESPONSE = "0x12";
	
	@Override
	public boolean isRequest(IoSession session, Object message) {
		LOG.info("请求心跳包信息: " + message);
		if (message.equals(HEARTBEATREQUEST))
			return true;
		return false;
	}
	
	@Override
	public boolean isResponse(IoSession session, Object message) {
		LOG.info("响应心跳包信息: " + message);
		if (message.equals(HEARTBEATRESPONSE))
			return true;
		return false;
	}
}	

设置心跳超时处理,实现KeepAliveRequestTimeoutHandler接口:

public class KeepAliveRequestTimeoutHandlerImpl implements KeepAliveRequestTimeoutHandler {
	public static Logger logger = LoggerFactory.getLogger(KeepAliveRequestTimeoutHandlerImpl.class);
	@Override
	public void keepAliveRequestTimedOut(KeepAliveFilter filter, IoSession session) throws Exception {
		logger.info("客户端:"+session.getId()+"已无响应");
		session.close(true);
	}

}

配置到acceptor中:


//心跳机制
KeepAliveFilter keepAliveFilter = new KeepAliveFilter(new KeepAliveMessageFactoryImpl());  
keepAliveFilter.setForwardEvent(false);  
keepAliveFilter.setRequestInterval(30);  
keepAliveFilter.setRequestTimeout(10);
//设置心跳超时
keepAliveFilter.setRequestTimeoutHandler(new KeepAliveRequestTimeoutHandlerImpl());
acceptor.getFilterChain().addLast("KeepAlive", keepAliveFilter); 

**注意:**配置要在acceptor.bind()方法之前执行,因为绑定套接字之后就不能再做这些准备工作了。

5. 数据包封装类
public class SocketBean implements Serializable{

	private static final long serialVersionUID = 1L;
	//数据唯一标志
	private UUID uid;
	//APP请求地址,便于内部直接转发
	private String serviceURL;
	//参数,json字符串
	private String param;
	//返回结果,json字符串
	private String result;
	//返回码,1成功,0失败
	private Integer code;
	//错误信息,code为1时可以为空
	private String errorMsg;
	//客户端是否第一次传输数据
	public Boolean firstSent;
	//客户端业务ID
	public Long clientBusinessId;
6. 数据包转发

外网app通过HTTP请求转发数据包,封装好请求的参数,客户端唯一标识(Socket Cilent),转发请求地址(内网app服务器地址),及参数。

@RequestMapping(value = "/socketRequest", produces = "application/json; charset=utf-8")
	@ResponseBody
	public JsonReturn socketRequest(HttpServletRequest request) {
		JsonReturn jsonReturn = new JsonReturn();
		try {
				// 获取请求参数
			String clientBusinessId = request.getHeader("clientBusinessId"); // 客户端ID
			String serviceURL = request.getHeader("serviceURL"); // 请求转发地址
			String haveFile = request.getHeader("haveFile"); // 是否包含文件
		    ...... //参数组装及文件处理(略)
			SocketBean sb = new SocketBean(Long.parseLong(clientBusinessId),serviceURL,jsonMap);
			String jsonString = JsonUtil.toJsonString(sb);
			logger.info("服务端传输参数:"+jsonString);
			session.write(jsonString);
			..... //异步处理客户端返回结果
			return jsonReturn ;//返回最终处理结果
		}
7. 服务开销问题(使用线程池)

服务端转发消息后,等待内网客户端返回结果。开启线程,用来获取客户端返回结果,线程池的大小取决于服务端吞吐量。
线程核心代码:

@Override
	public SocketBean call() throws Exception {
		SocketBean object = null;
		// 轮询开始时间
		long startWaitTime = System.currentTimeMillis();
		// 轮询抓取客户端匹配信息
		log.info("_____FetchResult1开始筛选返回结果,开始时间:" + startWaitTime + ",流水号:"
				+ JsonUtil.toJsonString(uid));
		//设置超时时间
		while (object == null && System.currentTimeMillis() - startWaitTime < MaxTime) {
			object = InitUtil.DataMap.get(uid); //客户端返回结果集
		}
		log.info("_____FetchResult2結束筛选,结束时间:" + System.currentTimeMillis() + ",流水号:"
				+ JsonUtil.toJsonString(uid)+"结果:"+JsonUtil.toJsonString(object));
		return object;
	}

异步处理客户端返回结果

// 筛选对应的结果返回
			Future<SocketBean> fetch = InitUtil.executorService.submit(new FetchResult(sb.getUid(), MaxTime));
			// 短暂的间隔一下,保证抓取客户端的结果在筛选返回结果之前
			Thread.sleep(100L);
			// 在限制时间内无法取到结果退出,避免线程阻塞
			SocketBean resultScoket = fetch.get(MaxTime, TimeUnit.MILLISECONDS);

猜你喜欢

转载自blog.csdn.net/lvdou_lvdou/article/details/78492444
今日推荐