socket基本原理参考:http://blog.youkuaiyun.com/hudashi/article/details/50790002
java socket点对点通信参考: http://baike.xsoftlab.net/view/71.html
mina框架参考:http://blog.youkuaiyun.com/ljx8928358/article/details/7759024
项目需求:外网内网数据交互(外网app客户端发送请求到内网客户端,抓取需求数据)。
首先需要考虑的问题:
-
服务器和客户端的连接保持
通过心跳机制定时监测连接是否断开。 -
客户端区分
每个客户端设置唯一标识,服务器端需要保存客户端的映射关系 -
数据包的封装
①数据包标识
②请求参数:token,url,业务参数等等。
考虑安全性使用加密。
③客户端唯一标识。 -
是否有文件传输
-
服务的开销(并发量的问题,因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);