新增对websocket的支持

添加web sdk,以及使用示例,文档
This commit is contained in:
xiajun 2017-10-30 13:52:15 +08:00
parent b101c46fbf
commit 2b3bd38f12
79 changed files with 1832 additions and 296 deletions

View File

@ -32,7 +32,7 @@ public class HeartbeatRequest implements Serializable,Protobufable {
private static final long serialVersionUID = 1L;
private static final String TAG = "SERVER_HEARTBEAT_REQUEST";
private static final String CMD_HEARTBEAT_RESPONSE = "SR";
private static final String CMD_HEARTBEAT_REQUEST = "SR";
private static HeartbeatRequest object = new HeartbeatRequest();
@ -46,7 +46,7 @@ public class HeartbeatRequest implements Serializable,Protobufable {
@Override
public byte[] getByteArray() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
return CMD_HEARTBEAT_REQUEST.getBytes();
}
public String toString(){

View File

@ -3,8 +3,9 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="D:\devtools\dev\gradle-3.4" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View File

@ -55,7 +55,7 @@
<!-- ****************************************CIM推送配置 end*************************************** -->
<!--消息接受广播注册-->
<receiver android:name="com.farsunset.ichat.example.receiver.CIMPushManagerReceiver" android:exported="false">
<receiver android:name="com.farsunset.ichat.example.receiver.CIMPushManagerReceiver">
<intent-filter android:priority="0x7fffffff">
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> <!-- 网络变化广播 -->
<action android:name="com.farsunset.cim.MESSAGE_RECEIVED"/><!-- 消息广播action -->

View File

@ -24,7 +24,7 @@ package com.farsunset.ichat.example.app;
public interface Constant {
//服务端IP地址
public static final String CIM_SERVER_HOST = "172.168.11.13";
public static final String CIM_SERVER_HOST = "172.168.11.18";
//注意这里的端口不是tomcat的端口CIM端口在服务端spring-cim.xml中配置的没改动就使用默认的23456

View File

@ -1,9 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="lib" path="libs/log4j-1.2.17.jar"/>
<classpathentry kind="lib" path="libs/mina-core-2.0.16.jar"/>
<classpathentry kind="lib" path="libs/protobuf-java-3.2.0.jar"/>
<classpathentry kind="lib" path="libs/fastjson-1.2.39.jar"/>
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.module.container"/>
<classpathentry kind="con" path="org.eclipse.jst.server.core.container/org.eclipse.jst.server.tomcat.runtimeTarget/Apache Tomcat v7.0">
<attributes>
<attribute name="owner.project.facets" value="jst.utility"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/jdk1.8.0_131">
<attributes>
<attribute name="owner.project.facets" value="java"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -5,13 +5,26 @@
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,7 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.6

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="cim-server-sdk">
<wb-resource deploy-path="/" source-path="/src"/>
</wb-module>
</project-modules>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<faceted-project>
<runtime name="Apache Tomcat v7.0"/>
<fixed facet="java"/>
<fixed facet="jst.utility"/>
<installed facet="java" version="1.6"/>
<installed facet="jst.utility" version="1.0"/>
</faceted-project>

Binary file not shown.

View File

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Class-Path:

View File

@ -46,19 +46,16 @@ public interface CIMConstant {
public static interface ProtobufType{
byte C_H_RS = 0;
byte S_H_RQ = 1;
byte C_H_RS = 0;
byte MESSAGE = 2;
byte SENTBODY = 3;
byte REPLYBODY = 4;
}
public static interface MessageAction{
//被其他设备登录挤下线消息
String ACTION_999 ="999";
//被系统禁用消息
String ACTION_444 ="444";
}
}

View File

@ -21,89 +21,18 @@
*/
package com.farsunset.cim.sdk.server.filter;
import org.apache.log4j.Logger;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.demux.DemuxingProtocolDecoder;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.HeartbeatResponse;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.model.proto.SentBodyProto;
import com.farsunset.cim.sdk.server.filter.decoder.AppMessageDecoder;
import com.farsunset.cim.sdk.server.filter.decoder.WebMessageDecoder;
/**
* 服务端接收消息解码
*/
public class ServerMessageDecoder extends CumulativeProtocolDecoder {
public class ServerMessageDecoder extends DemuxingProtocolDecoder {
protected final Logger logger = Logger.getLogger(ServerMessageDecoder.class);
@Override
public boolean doDecode(IoSession iosession, IoBuffer iobuffer, ProtocolDecoderOutput out) throws Exception {
/**
* 消息头3位
*/
if(iobuffer.remaining() < CIMConstant.DATA_HEADER_LENGTH){
return false;
}
iobuffer.mark();
byte conetnType = iobuffer.get();
byte lv =iobuffer.get();//int 低位
byte hv =iobuffer.get();//int 高位
int conetnLength = getContentLength(lv,hv);
//如果消息体没有接收完整则重置读取等待下一次重新读取
if(conetnLength > iobuffer.remaining()){
iobuffer.reset();
return false;
}
byte[] dataBytes = new byte[conetnLength];
iobuffer.get(dataBytes, 0, conetnLength);
Object message = mappingMessageObject(dataBytes,conetnType);
if(message != null){
out.write(message);
}
return true;
}
public Object mappingMessageObject(byte[] data,byte type) throws Exception
{
if(CIMConstant.ProtobufType.C_H_RS == type)
{
HeartbeatResponse response = HeartbeatResponse.getInstance();
logger.info(response.toString());
return response;
}
if(CIMConstant.ProtobufType.SENTBODY == type)
{
SentBodyProto.Model bodyProto = SentBodyProto.Model.parseFrom(data);
SentBody body = new SentBody();
body.setKey(bodyProto.getKey());
body.setTimestamp(bodyProto.getTimestamp());
body.putAll(bodyProto.getDataMap());
logger.info(body.toString());
return body;
}
return null;
}
/**
* 解析消息体长度
* @param type
* @param length
* @return
*/
private int getContentLength(byte lv,byte hv){
int l = (lv & 0xff);
int h = (hv & 0xff);
return (l| (h <<= 8));
public ServerMessageDecoder() {
addMessageDecoder(new AppMessageDecoder());
addMessageDecoder(new WebMessageDecoder());
}
}

View File

@ -26,10 +26,10 @@ import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.Protobufable;
import com.farsunset.cim.sdk.server.model.WebsocketResponse;
import com.farsunset.cim.sdk.server.model.feature.EncodeFormatable;
import com.farsunset.cim.sdk.server.session.CIMSession;
/**
* 服务端发送消息前编码
@ -37,41 +37,118 @@ import com.farsunset.cim.sdk.server.model.Protobufable;
public class ServerMessageEncoder extends ProtocolEncoderAdapter {
protected final Logger logger = Logger.getLogger(ServerMessageEncoder.class);
@Override
public void encode(IoSession iosession, Object object, ProtocolEncoderOutput out) throws Exception {
if(object instanceof Protobufable){
Protobufable data = (Protobufable) object;
byte[] byteArray = data.getByteArray();
IoBuffer buff = IoBuffer.allocate(byteArray.length + CIMConstant.DATA_HEADER_LENGTH).setAutoExpand(true);
buff.put(createHeader(data.getType(),byteArray.length));
buff.put(byteArray);
buff.flip();
Object channel = iosession.getAttribute("channel");
/**
* websocket的握手响应
*/
if (CIMSession.CHANNEL_BROWSER.equals(channel) && object instanceof WebsocketResponse) {
WebsocketResponse data = (WebsocketResponse) object;
byte[] byteArray = data.getBytes();
IoBuffer buff = IoBuffer.allocate(byteArray.length).setAutoExpand(true);
buff.put(byteArray);
buff.flip();
out.write(buff);
//打印出收到的消息
logger.info(data.toString());
}
}
/**
* websocket的数据传输使用JSON编码数据格式因为Protobuf还没有支持js
*/
if (CIMSession.CHANNEL_BROWSER.equals(channel) && object instanceof EncodeFormatable) {
EncodeFormatable data = (EncodeFormatable) object;
byte[] byteArray = encodeDataFrame(data.getJSONBody());
IoBuffer buff = IoBuffer.allocate(byteArray.length).setAutoExpand(true);
/**
* 由于websocket没有黏包和断包的问题所以不必知道消息体的大小
*/
buff.put(byteArray);
buff.flip();
out.write(buff);
logger.info(data.toString());
}
/**
* 非websocket的数据传输使用Protobuf编码数据格式
*/
if (!CIMSession.CHANNEL_BROWSER.equals(channel) && object instanceof EncodeFormatable) {
EncodeFormatable data = (EncodeFormatable) object;
byte[] byteArray = data.getProtobufBody();
IoBuffer buff = IoBuffer.allocate(byteArray.length + CIMConstant.DATA_HEADER_LENGTH).setAutoExpand(true);
buff.put(createHeader(data.getDataType(), byteArray.length));
buff.put(byteArray);
buff.flip();
out.write(buff);
logger.info(data.toString());
}
}
/**
* 消息体最大为65535
*
* @param type
* @param length
* @return
*/
private byte[] createHeader(byte type,int length){
private byte[] createHeader(byte type, int length) {
byte[] header = new byte[CIMConstant.DATA_HEADER_LENGTH];
header[0] = type;
header[1] = (byte) (length & 0xff);
header[2] = (byte) ((length >> 8) & 0xff);
header[2] = (byte) ((length >> 8) & 0xff);
return header;
}
/**
* 发送到websocket的数据需要进行相关格式转换
* 对传入数据进行无掩码转换
* @param data
* @return
*/
public static byte[] encodeDataFrame(byte[] data) {
// 掩码开始位置
int maskIndex = 2;
// 计算掩码开始位置
if (data.length <= 125) {
maskIndex = 2;
} else if (data.length > 65536) {
maskIndex = 10;
} else if (data.length > 125) {
maskIndex = 4;
}
// 创建返回数据
byte[] result = new byte[data.length + maskIndex];
// 开始计算ws-frame
// frame-fin + frame-rsv1 + frame-rsv2 + frame-rsv3 + frame-opcode
result[0] = (byte) 0x81; // 129
// frame-masked+frame-payload-length
// 从第9个字节开始是 1111101=125,掩码是第3-第6个数据
// 从第9个字节开始是 1111110>=126,掩码是第5-第8个数据
if (data.length <= 125) {
result[1] = (byte) (data.length);
} else if (data.length > 65536) {
result[1] = 0x7F; // 127
} else if (data.length > 125) {
result[1] = 0x7E; // 126
result[2] = (byte) (data.length >> 8);
result[3] = (byte) (data.length % 256);
}
// 将数据编码放到最后
for (int i = 0; i < data.length; i++) {
result[i + maskIndex] = data[i];
}
return result;
}
}

View File

@ -0,0 +1,126 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.filter.decoder;
import org.apache.log4j.Logger;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.demux.MessageDecoderAdapter;
import org.apache.mina.filter.codec.demux.MessageDecoderResult;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.HeartbeatResponse;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.model.proto.SentBodyProto;
/**
* 服务端接收消息解码
*/
public class AppMessageDecoder extends MessageDecoderAdapter {
protected final Logger logger = Logger.getLogger(AppMessageDecoder.class);
@Override
public MessageDecoderResult decodable(IoSession arg0, IoBuffer iobuffer) {
if(iobuffer.remaining() < CIMConstant.DATA_HEADER_LENGTH){
return NEED_DATA;
}
/**
* 原生SDK只会发送2种类型消息 1个心跳类型 另一个是sendbody报文的第一个字节为消息类型
*/
byte conetnType = iobuffer.get();
if(conetnType == CIMConstant.ProtobufType.C_H_RS || conetnType == CIMConstant.ProtobufType.SENTBODY) {
return OK;
}
return NOT_OK;
}
@Override
public MessageDecoderResult decode(IoSession iosession, IoBuffer iobuffer, ProtocolDecoderOutput out) throws Exception {
iobuffer.mark();
byte conetnType = iobuffer.get();
byte lv =iobuffer.get();//int 低位
byte hv =iobuffer.get();//int 高位
int conetnLength = getContentLength(lv,hv);
//如果消息体没有接收完整则重置读取等待下一次重新读取
if(conetnLength > iobuffer.remaining()){
iobuffer.reset();
return NEED_DATA;
}
byte[] dataBytes = new byte[conetnLength];
iobuffer.get(dataBytes, 0, conetnLength);
Object message = mappingMessageObject(dataBytes,conetnType);
if(message != null){
out.write(message);
}
return OK;
}
public Object mappingMessageObject(byte[] data,byte type) throws Exception
{
if(CIMConstant.ProtobufType.C_H_RS == type)
{
HeartbeatResponse response = HeartbeatResponse.getInstance();
logger.info(response.toString());
return response;
}
if(CIMConstant.ProtobufType.SENTBODY == type)
{
SentBodyProto.Model bodyProto = SentBodyProto.Model.parseFrom(data);
SentBody body = new SentBody();
body.setKey(bodyProto.getKey());
body.setTimestamp(bodyProto.getTimestamp());
body.putAll(bodyProto.getDataMap());
logger.info(body.toString());
return body;
}
return null;
}
/**
* 解析消息体长度
* @param type
* @param length
* @return
*/
private int getContentLength(byte lv,byte hv){
int l = (lv & 0xff);
int h = (hv & 0xff);
return (l| (h <<= 8));
}
}

View File

@ -0,0 +1,171 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.filter.decoder;
import java.io.UnsupportedEncodingException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.log4j.Logger;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.demux.MessageDecoderAdapter;
import org.apache.mina.filter.codec.demux.MessageDecoderResult;
import com.alibaba.fastjson.JSON;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.handler.CIMNioSocketAcceptor;
import com.farsunset.cim.sdk.server.model.HeartbeatResponse;
import com.farsunset.cim.sdk.server.model.SentBody;
/**
* 服务端接收消息解码
*/
public class WebMessageDecoder extends MessageDecoderAdapter {
public static final byte MASK = 0x1;// 1000 0000
public static final byte HAS_EXTEND_DATA = 126;
public static final byte HAS_EXTEND_DATA_CONTINUE = 127;
public static final byte PAYLOADLEN = 0x7F;// 0111 1111
public static final Pattern SEC_KEY_PATTERN = Pattern.compile("^(Sec-WebSocket-Key:).+",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
protected final Logger logger = Logger.getLogger(WebMessageDecoder.class);
@Override
public MessageDecoderResult decodable(IoSession arg0, IoBuffer iobuffer) {
if (iobuffer.remaining() < 2) {
return NEED_DATA;
}
/**
* 原生SDK只会发送2种类型消息 1个心跳类型 另一个是sendbody报文的第一个字节为消息类型
* 如果非原生sdk发出的消息则认为是websocket发送的消息
* websocket发送的消息 第一个字节不可能等于C_H_RS或者SENTBODY
*/
byte conetnType = iobuffer.get();
if (conetnType == CIMConstant.ProtobufType.C_H_RS || conetnType == CIMConstant.ProtobufType.SENTBODY) {
return NOT_OK;
}
byte head = iobuffer.get();// 第二个字节
byte datalength = (byte) (head & PAYLOADLEN);// 得到第二个字节后七位的值
int length = 0;
if (datalength < HAS_EXTEND_DATA) {// 第一种是消息内容少于126存储消息长度
length = datalength;
} else if (datalength == HAS_EXTEND_DATA) {// 第二种是消息长度大于等于126且少于UINT16的情况此值为126
if (iobuffer.remaining() < 2) {
return NEED_DATA;
}
byte[] extended = new byte[2];
iobuffer.get(extended);
int shift = 0;
length = 0;
for (int i = extended.length - 1; i >= 0; i--) {
length = length + ((extended[i] & 0xFF) << shift);
shift += 8;
}
} else if (datalength == HAS_EXTEND_DATA_CONTINUE) {// 第三种是消息长度大于UINT16的情况下此值为127
if (iobuffer.remaining() < 4) {
return NEED_DATA;
}
byte[] extended = new byte[4];
iobuffer.get(extended);
int shift = 0;
length = 0;
for (int i = extended.length - 1; i >= 0; i--) {
length = length + ((extended[i] & 0xFF) << shift);
shift += 8;
}
}
int ismask = head >> 7 & MASK;// 得到第二个字节第一位的值
if ((ismask == 1 && iobuffer.remaining() < 4 + length) || (ismask == 0 && iobuffer.remaining() < length)) {// 有掩码
return NEED_DATA;
}
return OK;
}
@Override
public MessageDecoderResult decode(IoSession iosession, IoBuffer in, ProtocolDecoderOutput out)throws Exception {
in.get();
byte head = in.get();
byte datalength = (byte) (head & PAYLOADLEN);
if (datalength < HAS_EXTEND_DATA) {
} else if (datalength == HAS_EXTEND_DATA) {
in.get(new byte[2]);
} else if (datalength == HAS_EXTEND_DATA_CONTINUE) {
in.get(new byte[4]);
}
int ismask = head >> 7 & MASK;
byte[] data = null;
if (ismask == 1) {// 有掩码
// 获取掩码
byte[] mask = new byte[4];
in.get(mask);
data = new byte[in.remaining()];
in.get(data);
for (int i = 0; i < data.length; i++) {
// 数据进行异或运算
data[i] = (byte) (data[i] ^ mask[i % 4]);
}
handleSentBodyAndHeartPing(data,out);
} else {
data = new byte[in.remaining()];
in.get(data);
handleWebsocketHandshake(new String(data, "UTF-8"),out);
}
return OK;
}
private void handleWebsocketHandshake(String message,ProtocolDecoderOutput out) {
SentBody body = new SentBody();
body.setKey(CIMNioSocketAcceptor.WEBSOCKET_HANDLER_KEY);
Matcher m = SEC_KEY_PATTERN.matcher(message);
if (m.find()) {
String foundstring = m.group();
body.put("key", foundstring.split(":")[1].trim());
}
out.write(body);
}
public void handleSentBodyAndHeartPing(byte[] data,ProtocolDecoderOutput out) throws UnsupportedEncodingException {
String message = new String(data, "UTF-8");
/**
* 只处理心跳响应以及sentbody消息
*/
if (HeartbeatResponse.CMD_HEARTBEAT_RESPONSE.equals(message)) {
HeartbeatResponse response = HeartbeatResponse.getInstance();
logger.info(response.toString());
out.write(response);
}else if(data.length > 2)
{
SentBody body = JSON.parseObject(message, SentBody.class);
logger.info(body.toString());
out.write(body);
}
}
}

View File

@ -49,7 +49,8 @@ import com.farsunset.cim.sdk.server.session.CIMSession;
public class CIMNioSocketAcceptor extends IoHandlerAdapter implements KeepAliveMessageFactory{
private final static String CIMSESSION_CLOSED_HANDLER_KEY = "client_cimsession_closed";
public final static String WEBSOCKET_HANDLER_KEY = "client_websocket_handshake";
public final static String CIMSESSION_CLOSED_HANDLER_KEY = "client_cimsession_closed";
private Logger logger = Logger.getLogger(CIMNioSocketAcceptor.class);
private HashMap<String, CIMRequestHandler> handlers = new HashMap<String, CIMRequestHandler>();
private IoAcceptor acceptor;
@ -60,6 +61,12 @@ public class CIMNioSocketAcceptor extends IoHandlerAdapter implements KeepAliveM
public void bind() throws IOException
{
/**
* 预制websocket握手请求的处理
*/
handlers.put(WEBSOCKET_HANDLER_KEY, new WebsocketHandler());
acceptor = new NioSocketAcceptor();
acceptor.getSessionConfig().setReadBufferSize(READ_BUFFER_SIZE);
((DefaultSocketSessionConfig)acceptor.getSessionConfig()).setKeepAlive(true);

View File

@ -0,0 +1,53 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.handler;
import java.security.MessageDigest;
import com.farsunset.cim.sdk.server.model.ReplyBody;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.model.WebsocketResponse;
import com.farsunset.cim.sdk.server.session.CIMSession;
/**
* 处理websocket握手请求返回响应的报文给浏览器
* @author Iraid
*
*/
public class WebsocketHandler implements CIMRequestHandler {
private final static String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
public ReplyBody process(CIMSession session, SentBody body) {
session.setChannel(CIMSession.CHANNEL_BROWSER);
String secKey = body.get("key") + GUID;
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(secKey.getBytes("iso-8859-1"), 0, secKey.length());
byte[] sha1Hash = md.digest();
secKey = new String(org.apache.mina.util.Base64.encodeBase64(sha1Hash));
} catch (Exception e) {
e.printStackTrace();
}
session.write(new WebsocketResponse(secKey));
return null;
}
}

View File

@ -22,13 +22,15 @@
package com.farsunset.cim.sdk.server.model;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.feature.EncodeFormatable;
/**
* 服务端心跳请求
*
*/
public class HeartbeatRequest implements Serializable,Protobufable {
public class HeartbeatRequest implements Serializable,EncodeFormatable {
private static final long serialVersionUID = 1L;
private static final String TAG = "SERVER_HEARTBEAT_REQUEST";
@ -45,17 +47,27 @@ public class HeartbeatRequest implements Serializable,Protobufable {
}
@Override
public byte[] getByteArray() {
public byte[] getProtobufBody() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
}
public String toString(){
return TAG;
}
@Override
public byte[] getJSONBody() {
try {
return CMD_HEARTBEAT_RESPONSE.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
@Override
public byte getType() {
public byte getDataType() {
return CIMConstant.ProtobufType.S_H_RQ;
}

View File

@ -23,16 +23,15 @@ package com.farsunset.cim.sdk.server.model;
import java.io.Serializable;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
/**
* 客户端心跳响应
*/
public class HeartbeatResponse implements Serializable, Protobufable {
public class HeartbeatResponse implements Serializable {
private static final long serialVersionUID = 1L;
private static final String TAG = "CLIENT_HEARTBEAT_RESPONSE";
private static final String CMD_HEARTBEAT_RESPONSE = "CR";
public static final String CMD_HEARTBEAT_RESPONSE = "CR";
private static HeartbeatResponse object = new HeartbeatResponse();
private HeartbeatResponse() {
@ -43,18 +42,10 @@ public class HeartbeatResponse implements Serializable, Protobufable {
return object;
}
@Override
public byte[] getByteArray() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
}
public String toString() {
return TAG;
}
@Override
public byte getType() {
return CIMConstant.ProtobufType.C_H_RS;
}
}

View File

@ -22,13 +22,15 @@
package com.farsunset.cim.sdk.server.model;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import com.alibaba.fastjson.JSONObject;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.feature.EncodeFormatable;
import com.farsunset.cim.sdk.server.model.proto.MessageProto;
/**
* 消息对象
*/
public class Message implements Serializable,Protobufable {
public class Message implements Serializable,EncodeFormatable {
private static final long serialVersionUID = 1L;
@ -74,6 +76,7 @@ public class Message implements Serializable,Protobufable {
private long timestamp;
public Message()
{
timestamp = System.currentTimeMillis();
@ -156,7 +159,6 @@ public class Message implements Serializable,Protobufable {
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("#Message#").append("\n");
buffer.append("mid:").append(mid).append("\n");
@ -175,7 +177,7 @@ public class Message implements Serializable,Protobufable {
return txt != null && txt.trim().length()!=0;
}
@Override
public byte[] getByteArray() {
public byte[] getProtobufBody() {
MessageProto.Model.Builder builder = MessageProto.Model.newBuilder();
builder.setMid(mid);
builder.setAction(action);
@ -200,10 +202,30 @@ public class Message implements Serializable,Protobufable {
}
return builder.build().toByteArray();
}
@Override
public byte getType() {
public byte[] getJSONBody() {
JSONObject json = new JSONObject();
json.put("contentType", getClass().getSimpleName());
json.put("mid", mid);
json.put("action", action);
json.put("title", title);
json.put("content", content);
json.put("extra", extra);
json.put("sender", sender);
json.put("receiver", receiver);
json.put("format", format);
json.put("timestamp", timestamp);
String data = json.toJSONString();
try {
return data.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
@Override
public byte getDataType() {
return CIMConstant.ProtobufType.MESSAGE;
}
}

View File

@ -22,17 +22,20 @@
package com.farsunset.cim.sdk.server.model;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import com.alibaba.fastjson.JSONObject;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.feature.EncodeFormatable;
import com.farsunset.cim.sdk.server.model.proto.ReplyBodyProto;
/**
* 请求应答对象
*
*/
public class ReplyBody implements Serializable ,Protobufable{
public class ReplyBody implements Serializable ,EncodeFormatable{
private static final long serialVersionUID = 1L;
@ -141,7 +144,7 @@ public class ReplyBody implements Serializable ,Protobufable{
return buffer.toString();
}
@Override
public byte[] getByteArray() {
public byte[] getProtobufBody() {
ReplyBodyProto.Model.Builder builder = ReplyBodyProto.Model.newBuilder();
builder.setCode(code);
if(message!=null){
@ -157,9 +160,24 @@ public class ReplyBody implements Serializable ,Protobufable{
}
@Override
public byte getType() {
// TODO Auto-generated method stub
public byte getDataType() {
return CIMConstant.ProtobufType.REPLYBODY;
}
@Override
public byte[] getJSONBody() {
JSONObject json = new JSONObject();
json.put("contentType", getClass().getSimpleName());
json.put("key", key);
json.put("code", code);
json.put("message", message);
json.put("data", data);
String data = json.toJSONString();
try {
return data.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}

View File

@ -35,7 +35,7 @@ public class SentBody implements Serializable {
private String key;
private HashMap<String, String> data = new HashMap<String, String>();
public HashMap<String, String> data = new HashMap<String, String>();
private long timestamp;

View File

@ -0,0 +1,61 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.model;
import java.io.UnsupportedEncodingException;
/**
*websocket握手响应结果
*
*/
public class WebsocketResponse{
private String token;
public WebsocketResponse(String token) {
this.token = token;
}
public byte[] getBytes() {
try {
return toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("HTTP/1.1 101 Switching Protocols");
builder.append("\r\n");
builder.append("Upgrade: websocket");
builder.append("\r\n");
builder.append("Connection: Upgrade");
builder.append("\r\n");
builder.append("Sec-WebSocket-Accept:").append(token);
builder.append("\r\n");
builder.append("\r\n");
return builder.toString();
}
}

View File

@ -19,13 +19,12 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.model;
package com.farsunset.cim.sdk.server.model.feature;
/**
* 需要向另一端发送的结构体
*/
public interface Protobufable {
byte[] getByteArray();
byte getType();
public interface EncodeFormatable {
byte[] getProtobufBody();
byte[] getJSONBody();
byte getDataType();
}

View File

@ -52,7 +52,8 @@ public class CIMSession implements Serializable{
public transient static String CHANNEL_ANDROID = "android";
public transient static String CHANNEL_WINDOWS = "windows";
public transient static String CHANNEL_WP = "wp";
public transient static String CHANNEL_BROWSER = "browser";
private transient IoSession session;
private String gid;//session全局ID

View File

@ -61,8 +61,6 @@ public class DefaultSessionManager implements SessionManager{
return sessions.get(account);
}
public List<CIMSession> queryAll() {
List<CIMSession> list = new ArrayList<CIMSession>();

View File

@ -3,6 +3,7 @@
<name>cim-server</name>
<comment></comment>
<projects>
<project>cim-server-sdk</project>
</projects>
<buildSpec>
<buildCommand>

View File

@ -3,6 +3,9 @@
<wb-resource deploy-path="/" source-path="/WebContent" tag="defaultRootSource"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resource"/>
<dependent-module archiveName="cim-server-sdk.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/cim-server-sdk/cim-server-sdk">
<dependency-type>uses</dependency-type>
</dependent-module>
<property name="context-root" value="cim-server"/>
<property name="java-output-path" value="/cim-server/build/classes"/>
</wb-module>

View File

@ -8,7 +8,14 @@
<div id="_main_nav" class="ui-vnav">
<ul class="ui-nav-inner">
<li style="height: 50px; text-align: center; margin-top: 10px;">
<div class="btn-group" style="margin-top: 5px;">
<a type="button" class="btn btn-danger" target="_blank"
href="javascript:openWebclient();">CIM for Web</a>
</div>
</li>
<li style="border-bottom: 1px solid #D1D6DA;"></li>
<li class="ui-item" id="sessionMenu">
<a href="<%=navBasePath%>/admin/session_list.action"> <span
class="ui-text">在线用户</span> <i class="ui-bg nav-recycle"></i> </a>

View File

@ -0,0 +1,109 @@
var CIM_URI="ws://127.0.0.1:23456";// 修改为服务器的真实IP
var CMD_HEARTBEAT_REQUEST = "SR";
var CMD_HEARTBEAT_RESPONSE = "CR";
var SDK_VERSION = "1.0.0";
var SDK_CHANNEL = "browser";
var APP_PACKAGE = "com.farsunset.webcim";
var ACCOUNT;
var socket;
var manualStop = false;
var CIMWebBridge = new Object();
CIMWebBridge.connection = function(){
manualStop = false;
socket = new WebSocket(CIM_URI);
socket.onopen = CIMWebBridge.innerOnConnectionSuccessed;
socket.onmessage = CIMWebBridge.innerOnMessageReceived;
socket.onclose = CIMWebBridge.innerOnConnectionClosed;
};
CIMWebBridge.bindAccount = function(id,deviceId){
ACCOUNT=id;
var browser = getBrowser();
var body = {};
body.key = "client_bind";
body.data = {};
body.data.account = id;
body.data.channel = SDK_CHANNEL;
body.data.version = SDK_VERSION;
body.data.osVersion = browser.version;
body.data.packageName = APP_PACKAGE;
body.data.deviceId = deviceId;
body.data.device = browser.name;
CIMWebBridge.sendRequest(body);
};
CIMWebBridge.stop = function(){
manualStop = true;
socket.close();
};
CIMWebBridge.innerOnConnectionSuccessed = function(){
if(typeof onConnectionSuccessed != 'undefined' && onConnectionSuccessed instanceof Function){
onConnectionSuccessed();
}
};
CIMWebBridge.innerOnMessageReceived = function(e){
var data = e.data;
/**
* 收到服务端发来的心跳请求立即回复响应否则服务端会在10秒后断开连接
*/
if(data == CMD_HEARTBEAT_REQUEST){
socket.send(CMD_HEARTBEAT_RESPONSE);
return
}
var json = JSON.parse(data);
if(json.contentType == "Message" && typeof onMessageReceived != 'undefined' && onMessageReceived instanceof Function){
onMessageReceived(json);
}
if(json.contentType == "ReplyBody" && typeof onReplyReceived != 'undefined' && onReplyReceived instanceof Function){
onReplyReceived(json);
}
};
CIMWebBridge.innerOnConnectionClosed = function(e){
if(!manualStop){
CIMWebBridge.connection();
}
};
CIMWebBridge.sendRequest = function(body){
var json = JSON.stringify(body);
socket.send(json);
};
function getBrowser() {
var explorer = window.navigator.userAgent.toLowerCase() ;
// ie
if (explorer.indexOf("msie") >= 0) {
var ver=explorer.match(/msie ([\d.]+)/)[1];
return {name:"IE",version:ver};
}
// firefox
else if (explorer.indexOf("firefox") >= 0) {
var ver=explorer.match(/firefox\/([\d.]+)/)[1];
return {name:"Firefox",version:ver};
}
// Chrome
else if(explorer.indexOf("chrome") >= 0){
var ver=explorer.match(/chrome\/([\d.]+)/)[1];
return {name:"Chrome",version:ver};
}
// Opera
else if(explorer.indexOf("opera") >= 0){
var ver=explorer.match(/opera.([\d.]+)/)[1];
return {name:"Opera",version:ver};
}
// Safari
else if(explorer.indexOf("Safari") >= 0){
var ver=explorer.match(/version\/([\d.]+)/)[1];
return {name:"Safari",version:ver};
}
return {name:"Other",version:"1.0.0"};
}

View File

@ -0,0 +1,50 @@
<%@ page language="java" pageEncoding="utf-8"%>
<%
String loginBasePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ request.getContextPath();
%>
<script type="text/javascript">
function doLogin()
{
ACCOUNT = $('#account').val();
if($.trim(ACCOUNT)=='' )
{
return;
}
showProcess('正在接入请稍后......');
CIMWebBridge.connection();
}
</script>
<div class="modal fade" id="LoginDialog" tabindex="-1" role="dialog" data-backdrop="static">
<div class="modal-dialog" style="width: 300px;margin: 30px auto;">
<div class="modal-content" >
<div class="modal-body" style="padding:0px;" >
<div style="height:150px;text-align: center; background: #5FA0D3; color: #ffffff; border: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px;">
<img src="<%=loginBasePath %>/resource/img/icon.png" style="height: 60px;width: 60px;margin-top:30px;"/>
<div style="margin-top: 20px; color: #ffffff;font-size: 16px;">使用用户帐号登录</div>
</div>
<div class="input-group" style="margin-top: 20px;margin-left:10px;margin-right:10px;margin-bottom:20px;">
<span class="input-group-addon"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></span>
<input type="text" class="form-control" id="account" maxlength="32" placeholder="帐号"
style="display: inline; width: 100%; height: 50px;" />
</div>
</div>
<div class="modal-footer" style="text-align: center;">
<a type="button" class="btn btn-success btn-lg" onclick="doLogin()"
style="width: 250px;">登录</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,70 @@
<%@ page language="java" pageEncoding="utf-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ path;
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta charset="utf-8"/>
<title>cim for web(beta) </title>
<link charset="utf-8" rel="stylesheet" href="<%=basePath%>/resource/bootstrap-3.3.6-dist/css/bootstrap.min.css" />
<link charset="utf-8" rel="stylesheet" href="<%=basePath%>/resource/css/dialog.css" />
<script type="text/javascript" src="<%=basePath%>/resource/js/jquery-2.2.3.min.js"></script>
<script type="text/javascript" src="<%=basePath%>/resource/bootstrap-3.3.6-dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="<%=basePath%>/resource/js/framework.js"></script>
<script type="text/javascript" src="cim.web.sdk.js"></script>
</head>
<script type="text/javascript">
/** 当socket连接成功回调 **/
function onConnectionSuccessed()
{
var sessionId = '<%=session.getId()%>';
CIMWebBridge.bindAccount($('#account').val(),sessionId);
}
/** 当收到请求回复时候回调 **/
function onReplyReceived(json)
{
if(json.key=='client_bind' && json.code==200)
{
hideProcess();
doHideDialog('LoginDialog');
doShowDialog('MessageDialog');
$("#current_account").text(ACCOUNT);
}
}
/** 当收到消息时候回调 **/
function onMessageReceived(message)
{
$("#messageList").append("<div class='alert alert-info' >"+message.content+"</div>");
}
$(document).ready(function(){
doShowDialog('LoginDialog');
});
</script>
<body style="background-color: rgb(190, 209, 216);width: 600px;">
<div id="global_mask" style="display: none; position: absolute; top: 0px; left: 0px; z-index: 998; background-color: rgb(190, 209, 216); opacity: 0.5; width: 100%; height: 100%; overflow: hidden; background-position: initial initial; background-repeat: initial initial;"></div>
<%@include file="loginDialog.jsp"%>
<%@include file="messageDialog.jsp"%>
</body>
</html>

View File

@ -0,0 +1,21 @@
<%@ page language="java" pageEncoding="utf-8"%>
<div class="modal fade" data-backdrop="static" id="MessageDialog" tabindex="-1" role="dialog" >
<div class="modal-dialog" style="width: 500px;margin: 30px auto;">
<div class="modal-content" >
<div class="modal-header" style="text-align: center;">
<h4>消息列表</h4>
<span style="float: left;">请在管理页面推送一条消息</span>
<span style="float: right;color: #4caf50;">当前帐号:<span id="current_account">10000</span></span>
</div>
<div class="modal-body" id="messageList" style="min-height: 600px;" >
</div>
</div>
</div>
</div>

View File

@ -45,7 +45,8 @@ public class SessionAction extends ActionSupport {
public String list()
{
ServletActionContext.getRequest().setAttribute("sessionList", ((DefaultSessionManager) ContextHolder.getBean("CIMSessionManager")).queryAll());
ServletActionContext.getRequest().setAttribute("sessionList", ((DefaultSessionManager) ContextHolder.getBean("CIMSessionManager")).queryAll());
return "list";
}

View File

@ -3,8 +3,9 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="D:\devtools\dev\gradle-3.4" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View File

@ -55,7 +55,7 @@
<!-- ****************************************CIM推送配置 end*************************************** -->
<!--消息接受广播注册-->
<receiver android:name="com.farsunset.ichat.example.receiver.CIMPushManagerReceiver" android:exported="false">
<receiver android:name="com.farsunset.ichat.example.receiver.CIMPushManagerReceiver">
<intent-filter android:priority="0x7fffffff">
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> <!-- 网络变化广播 -->
<action android:name="com.farsunset.cim.MESSAGE_RECEIVED"/><!-- 消息广播action -->

View File

@ -24,7 +24,7 @@ package com.farsunset.ichat.example.app;
public interface Constant {
//服务端IP地址
public static final String CIM_SERVER_HOST = "172.168.141.13";
public static final String CIM_SERVER_HOST = "172.168.11.18";
//注意这里的端口不是tomcat的端口CIM端口在服务端spring-cim.xml中配置的没改动就使用默认的23456

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="lib" path="libs/log4j-1.2.17.jar"/>
<classpathentry kind="lib" path="libs/protobuf-java-3.2.0.jar"/>
<classpathentry kind="lib" path="libs/netty-buffer-4.1.9.Final.jar"/>
@ -9,5 +8,19 @@
<classpathentry kind="lib" path="libs/netty-common-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-handler-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-transport-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="libs/fastjson-1.2.39.jar"/>
<classpathentry kind="lib" path="libs/netty-codec-http-4.1.16.Final-sources.jar"/>
<classpathentry kind="lib" path="libs/netty-codec-http-4.1.16.Final.jar" sourcepath="libs/netty-codec-http-4.1.16.Final-sources.jar"/>
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.module.container"/>
<classpathentry kind="con" path="org.eclipse.jst.server.core.container/org.eclipse.jst.server.tomcat.runtimeTarget/Apache Tomcat v7.0">
<attributes>
<attribute name="owner.project.facets" value="jst.utility"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/jdk1.8.0_131">
<attributes>
<attribute name="owner.project.facets" value="java"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -5,13 +5,26 @@
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,7 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.6

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="cim-server-sdk">
<wb-resource deploy-path="/" source-path="/src"/>
</wb-module>
</project-modules>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<faceted-project>
<runtime name="Apache Tomcat v7.0"/>
<fixed facet="java"/>
<fixed facet="jst.utility"/>
<installed facet="java" version="1.6"/>
<installed facet="jst.utility" version="1.0"/>
</faceted-project>

Binary file not shown.

View File

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Class-Path:

View File

@ -48,7 +48,8 @@ public interface CIMConstant {
//消息头长度为3个字节第一个字节为消息类型第二第三字节 转换int后为消息长度
int DATA_HEADER_LENGTH = 3;
//WEBSOCKET消息头长度为2个字节
int WS_DATA_HEADER_LENGTH = 2;
public static interface ProtobufType{
byte C_H_RS = 0;
byte S_H_RQ = 1;

View File

@ -23,93 +23,42 @@ package com.farsunset.cim.sdk.server.filter;
import java.util.List;
import org.apache.log4j.Logger;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.HeartbeatResponse;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.model.proto.SentBodyProto;
import com.farsunset.cim.sdk.server.filter.decoder.AppMessageDecoder;
import com.farsunset.cim.sdk.server.filter.decoder.WebMessageDecoder;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
/**
* 服务端接收消息解码
* 服务端接收消息路由解码通过消息类型分发到不同的真正解码器
*/
public class ServerMessageDecoder extends ByteToMessageDecoder {
protected final Logger logger = Logger.getLogger(ServerMessageDecoder.class);
private WebMessageDecoder webMessageDecoder;
private AppMessageDecoder appMessageDecoder;
public ServerMessageDecoder() {
webMessageDecoder = new WebMessageDecoder();
appMessageDecoder = new AppMessageDecoder();
}
@Override
protected void decode(ChannelHandlerContext arg0, ByteBuf buffer, List<Object> queue) throws Exception {
/**
* 消息头3位
*/
if (buffer.readableBytes() < CIMConstant.DATA_HEADER_LENGTH) {
return;
}
buffer.markReaderIndex();
buffer.markReaderIndex();
byte conetnType = buffer.readByte();
byte lv = buffer.readByte();// int 低位
byte hv = buffer.readByte();// int 高位
int conetnLength = getContentLength(lv, hv);
// 如果消息体没有接收完整则重置读取等待下一次重新读取
if (conetnLength > buffer.readableBytes()) {
buffer.resetReaderIndex();
return;
}
byte[] dataBytes = new byte[conetnLength];
buffer.readBytes(dataBytes);
Object message = mappingMessageObject(dataBytes,conetnType);
if(message != null){
queue.add(message);
buffer.resetReaderIndex();
/**
* 原生SDK只会发送2种类型消息 1个心跳类型 另一个是sendbody报文的第一个字节为消息类型,否则才是websocket的消息
*/
if(conetnType == CIMConstant.ProtobufType.C_H_RS || conetnType == CIMConstant.ProtobufType.SENTBODY) {
appMessageDecoder.decode(arg0, buffer, queue);
}else
{
webMessageDecoder.decode(arg0, buffer, queue);
}
}
public Object mappingMessageObject(byte[] data,byte type) throws Exception
{
if(CIMConstant.ProtobufType.C_H_RS == type)
{
HeartbeatResponse response = HeartbeatResponse.getInstance();
logger.info(response.toString());
SentBody body = new SentBody();
body.setKey(CIMConstant.CLIENT_HEARTBEAT);
body.setTimestamp(System.currentTimeMillis());
return body;
}
if(CIMConstant.ProtobufType.SENTBODY == type)
{
SentBodyProto.Model bodyProto = SentBodyProto.Model.parseFrom(data);
SentBody body = new SentBody();
body.setKey(bodyProto.getKey());
body.setTimestamp(bodyProto.getTimestamp());
body.putAll(bodyProto.getDataMap());
logger.info(body.toString());
return body;
}
return null;
}
/**
* 解析消息体长度
* @param type
* @param length
* @return
*/
private int getContentLength(byte lv,byte hv){
int l = (lv & 0xff);
int h = (hv & 0xff);
return (l| (h <<= 8));
}
}

View File

@ -24,11 +24,14 @@ package com.farsunset.cim.sdk.server.filter;
import org.apache.log4j.Logger;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.Protobufable;
import com.farsunset.cim.sdk.server.model.WebsocketResponse;
import com.farsunset.cim.sdk.server.model.feature.EncodeFormatable;
import com.farsunset.cim.sdk.server.session.CIMSession;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.util.AttributeKey;
/**
@ -39,21 +42,45 @@ public class ServerMessageEncoder extends MessageToByteEncoder<Object> {
protected final Logger logger = Logger.getLogger(ServerMessageEncoder.class.getSimpleName());
@Override
protected void encode(ChannelHandlerContext ctx, Object message, ByteBuf out) throws Exception {
protected void encode(ChannelHandlerContext ctx, Object object, ByteBuf out) throws Exception {
Object channel = ctx.channel().attr(AttributeKey.valueOf("channel")).get();
/**
* websocket的握手响应
*/
if (CIMSession.CHANNEL_BROWSER.equals(channel) && object instanceof WebsocketResponse) {
WebsocketResponse data = (WebsocketResponse) object;
byte[] byteArray = data.getBytes();
out.writeBytes(byteArray);
logger.info(data.toString());
}
if(message instanceof Protobufable){
Protobufable data = (Protobufable) message;
byte[] byteArray = data.getByteArray();
out.writeBytes(createHeader(data.getType(),byteArray.length));
out.writeBytes(byteArray);
}
logger.info(message.toString());
/**
* websocket的数据传输使用JSON编码数据格式因为Protobuf还没有支持js
*/
if (CIMSession.CHANNEL_BROWSER.equals(channel) && object instanceof EncodeFormatable) {
EncodeFormatable data = (EncodeFormatable) object;
byte[] byteArray = encodeDataFrame(data.getJSONBody());
/**
* 由于websocket没有黏包和断包的问题所以不必知道消息体的大小
*/
out.writeBytes(byteArray);
logger.info(data.toString());
}
/**
* 非websocket的数据传输使用Protobuf编码数据格式
*/
if (!CIMSession.CHANNEL_BROWSER.equals(channel) && object instanceof EncodeFormatable) {
EncodeFormatable data = (EncodeFormatable) object;
byte[] byteArray = data.getProtobufBody();
out.writeBytes(createHeader(data.getDataType(),byteArray.length));
out.writeBytes(byteArray);
logger.info(data.toString());
}
}
/**
@ -70,5 +97,52 @@ public class ServerMessageEncoder extends MessageToByteEncoder<Object> {
return header;
}
/**
* 发送到websocket的数据需要进行相关格式转换
* 对传入数据进行无掩码转换
* @param data
* @return
*/
public static byte[] encodeDataFrame(byte[] data) {
// 掩码开始位置
int maskIndex = 2;
// 计算掩码开始位置
if (data.length <= 125) {
maskIndex = 2;
} else if (data.length > 65536) {
maskIndex = 10;
} else if (data.length > 125) {
maskIndex = 4;
}
// 创建返回数据
byte[] result = new byte[data.length + maskIndex];
// 开始计算ws-frame
// frame-fin + frame-rsv1 + frame-rsv2 + frame-rsv3 + frame-opcode
result[0] = (byte) 0x81; // 129
// frame-masked+frame-payload-length
// 从第9个字节开始是 1111101=125,掩码是第3-第6个数据
// 从第9个字节开始是 1111110>=126,掩码是第5-第8个数据
if (data.length <= 125) {
result[1] = (byte) (data.length);
} else if (data.length > 65536) {
result[1] = 0x7F; // 127
} else if (data.length > 125) {
result[1] = 0x7E; // 126
result[2] = (byte) (data.length >> 8);
result[3] = (byte) (data.length % 256);
}
// 将数据编码放到最后
for (int i = 0; i < data.length; i++) {
result[i + maskIndex] = data[i];
}
return result;
}
}

View File

@ -0,0 +1,113 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.filter.decoder;
import java.util.List;
import org.apache.log4j.Logger;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.HeartbeatResponse;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.model.proto.SentBodyProto;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
/**
* 服务端接收来自应用的消息解码
*/
public class AppMessageDecoder extends ByteToMessageDecoder {
protected final Logger logger = Logger.getLogger(AppMessageDecoder.class);
@Override
public void decode(ChannelHandlerContext arg0, ByteBuf buffer, List<Object> queue) throws Exception {
/**
* 消息头3位
*/
if (buffer.readableBytes() < CIMConstant.DATA_HEADER_LENGTH) {
return;
}
buffer.markReaderIndex();
byte conetnType = buffer.readByte();
byte lv = buffer.readByte();// int 低位
byte hv = buffer.readByte();// int 高位
int conetnLength = getContentLength(lv, hv);
// 如果消息体没有接收完整则重置读取等待下一次重新读取
if (conetnLength > buffer.readableBytes()) {
buffer.resetReaderIndex();
return;
}
byte[] dataBytes = new byte[conetnLength];
buffer.readBytes(dataBytes);
Object message = mappingMessageObject(dataBytes,conetnType);
if(message != null){
queue.add(message);
}
}
public Object mappingMessageObject(byte[] data,byte type) throws Exception
{
if(CIMConstant.ProtobufType.C_H_RS == type)
{
HeartbeatResponse response = HeartbeatResponse.getInstance();
logger.info(response.toString());
SentBody body = new SentBody();
body.setKey(CIMConstant.CLIENT_HEARTBEAT);
body.setTimestamp(System.currentTimeMillis());
return body;
}
if(CIMConstant.ProtobufType.SENTBODY == type)
{
SentBodyProto.Model bodyProto = SentBodyProto.Model.parseFrom(data);
SentBody body = new SentBody();
body.setKey(bodyProto.getKey());
body.setTimestamp(bodyProto.getTimestamp());
body.putAll(bodyProto.getDataMap());
logger.info(body.toString());
return body;
}
return null;
}
/**
* 解析消息体长度
* @param type
* @param length
* @return
*/
private int getContentLength(byte lv,byte hv){
int l = (lv & 0xff);
int h = (hv & 0xff);
return (l| (h <<= 8));
}
}

View File

@ -0,0 +1,180 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.filter.decoder;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.log4j.Logger;
import com.alibaba.fastjson.JSON;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.handler.CIMNioSocketAcceptor;
import com.farsunset.cim.sdk.server.model.HeartbeatResponse;
import com.farsunset.cim.sdk.server.model.SentBody;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
/**
* 服务端接收来自websocket消息解码
*/
public class WebMessageDecoder extends ByteToMessageDecoder {
public static final byte MASK = 0x1;// 1000 0000
public static final byte HAS_EXTEND_DATA = 126;
public static final byte HAS_EXTEND_DATA_CONTINUE = 127;
public static final byte PAYLOADLEN = 0x7F;// 0111 1111
public static final Pattern SEC_KEY_PATTERN = Pattern.compile("^(Sec-WebSocket-Key:).+",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
protected final Logger logger = Logger.getLogger(WebMessageDecoder.class);
@Override
public void decode(ChannelHandlerContext arg0, ByteBuf iobuffer, List<Object> queue) throws Exception {
/**
* 消息头2位
*/
if (iobuffer.readableBytes() < CIMConstant.WS_DATA_HEADER_LENGTH) {
return;
}
iobuffer.markReaderIndex();
byte head = iobuffer.readByte();// 第二个字节
byte datalength = (byte) (head & PAYLOADLEN);// 得到第二个字节后七位的值
int length = 0;
if (datalength < HAS_EXTEND_DATA) {// 第一种是消息内容少于126存储消息长度
length = datalength;
} else if (datalength == HAS_EXTEND_DATA) {// 第二种是消息长度大于等于126且少于UINT16的情况此值为126
if (iobuffer.readableBytes() < 2) {
iobuffer.resetReaderIndex();
return ;
}
byte[] extended = new byte[2];
iobuffer.readBytes(extended);
int shift = 0;
length = 0;
for (int i = extended.length - 1; i >= 0; i--) {
length = length + ((extended[i] & 0xFF) << shift);
shift += 8;
}
} else if (datalength == HAS_EXTEND_DATA_CONTINUE) {// 第三种是消息长度大于UINT16的情况下此值为127
if (iobuffer.readableBytes() < 4) {
iobuffer.resetReaderIndex();
return ;
}
byte[] extended = new byte[4];
iobuffer.readBytes(extended);
int shift = 0;
length = 0;
for (int i = extended.length - 1; i >= 0; i--) {
length = length + ((extended[i] & 0xFF) << shift);
shift += 8;
}
}
int ismask = head >> 7 & MASK;// 得到第二个字节第一位的值
if ((ismask == 1 && iobuffer.readableBytes() < 4 + length) || (ismask == 0 && iobuffer.readableBytes() < length)) {// 有掩码
iobuffer.resetReaderIndex();
return ;
}
iobuffer.resetReaderIndex();
decodeDataBody(iobuffer,queue);
}
public void decodeDataBody(ByteBuf iobuffer, List<Object> queue) {
iobuffer.readByte();
byte head = iobuffer.readByte();
byte datalength = (byte) (head & PAYLOADLEN);
if (datalength < HAS_EXTEND_DATA) {
} else if (datalength == HAS_EXTEND_DATA) {
iobuffer.readBytes(new byte[2]);
} else if (datalength == HAS_EXTEND_DATA_CONTINUE) {
iobuffer.readBytes(new byte[4]);
}
int ismask = head >> 7 & MASK;
byte[] data = null;
if (ismask == 1) {// 有掩码
// 获取掩码
byte[] mask = new byte[4];
iobuffer.readBytes(mask);
data = new byte[iobuffer.readableBytes()];
iobuffer.readBytes(data);
for (int i = 0; i < data.length; i++) {
// 数据进行异或运算
data[i] = (byte) (data[i] ^ mask[i % 4]);
}
handleSentBodyAndHeartPing(data,queue);
} else {
data = new byte[iobuffer.readableBytes()];
iobuffer.readBytes(data);
handleWebsocketHandshake(data,queue);
}
}
private void handleWebsocketHandshake(byte[] data,List<Object> queue) {
String message = null;
try {
message = new String(data, "UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
SentBody body = new SentBody();
body.setKey(CIMNioSocketAcceptor.WEBSOCKET_HANDLER_KEY);
Matcher m = SEC_KEY_PATTERN.matcher(message);
if (m.find()) {
String foundstring = m.group();
body.put("key", foundstring.split(":")[1].trim());
}
queue.add(body);
}
public void handleSentBodyAndHeartPing(byte[] data,List<Object> queue) {
String message = null;
try {
message = new String(data, "UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
/**
* 只处理心跳响应以及sentbody消息
*/
if (HeartbeatResponse.CMD_HEARTBEAT_RESPONSE.equals(message)) {
HeartbeatResponse response = HeartbeatResponse.getInstance();
logger.info(response.toString());
queue.add(response);
}else if(data.length > 2)
{
SentBody body = JSON.parseObject(message, SentBody.class);
logger.info(body.toString());
queue.add(body);
}
}
}

View File

@ -50,7 +50,7 @@ import io.netty.util.AttributeKey;
@Sharable
public class CIMNioSocketAcceptor extends SimpleChannelInboundHandler<SentBody>{
public final static String WEBSOCKET_HANDLER_KEY = "client_websocket_handshake";
private final static String CIMSESSION_CLOSED_HANDLER_KEY = "client_cimsession_closed";
private Logger logger = Logger.getLogger(CIMNioSocketAcceptor.class);
private HashMap<String, CIMRequestHandler> handlers = new HashMap<String, CIMRequestHandler>();
@ -66,6 +66,11 @@ public class CIMNioSocketAcceptor extends SimpleChannelInboundHandler<SentBody>{
public void bind() throws IOException
{
/**
* 预制websocket握手请求的处理
*/
handlers.put(WEBSOCKET_HANDLER_KEY, new WebsocketHandler());
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup());
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
@ -73,7 +78,6 @@ public class CIMNioSocketAcceptor extends SimpleChannelInboundHandler<SentBody>{
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ServerMessageDecoder());
ch.pipeline().addLast(new ServerMessageEncoder());
ch.pipeline().addLast(new IdleStateHandler(READ_IDLE_TIME,WRITE_IDLE_TIME,0));

View File

@ -0,0 +1,54 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.handler;
import java.security.MessageDigest;
import java.util.Base64;
import com.farsunset.cim.sdk.server.model.ReplyBody;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.model.WebsocketResponse;
import com.farsunset.cim.sdk.server.session.CIMSession;
/**
* 处理websocket握手请求返回响应的报文给浏览器
* @author Iraid
*
*/
public class WebsocketHandler implements CIMRequestHandler {
private final static String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
public ReplyBody process(CIMSession session, SentBody body) {
session.setChannel(CIMSession.CHANNEL_BROWSER);
String secKey = body.get("key") + GUID;
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(secKey.getBytes("iso-8859-1"), 0, secKey.length());
byte[] sha1Hash = md.digest();
secKey = new String(Base64.getEncoder().encode(sha1Hash));
} catch (Exception e) {
e.printStackTrace();
}
session.write(new WebsocketResponse(secKey));
return null;
}
}

View File

@ -24,16 +24,16 @@ package com.farsunset.cim.sdk.server.model;
import java.io.Serializable;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.feature.EncodeFormatable;
/**
* 服务端心跳请求
*
*/
public class HeartbeatRequest implements Serializable,Protobufable {
public class HeartbeatRequest implements Serializable,EncodeFormatable {
private static final long serialVersionUID = 1L;
private static final String TAG = "SERVER_HEARTBEAT_REQUEST";
private static final String CMD_HEARTBEAT_RESPONSE = "SR";
private static HeartbeatRequest object = new HeartbeatRequest();
private HeartbeatRequest(){
@ -44,18 +44,22 @@ public class HeartbeatRequest implements Serializable,Protobufable {
return object;
}
@Override
public byte[] getByteArray() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
}
public String toString(){
return TAG;
}
@Override
public byte[] getProtobufBody() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
}
@Override
public byte getType() {
public byte[] getJSONBody() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
}
@Override
public byte getDataType() {
return CIMConstant.ProtobufType.S_H_RQ;
}

View File

@ -23,16 +23,15 @@ package com.farsunset.cim.sdk.server.model;
import java.io.Serializable;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
/**
* 客户端心跳响应
*/
public class HeartbeatResponse implements Serializable, Protobufable {
public class HeartbeatResponse implements Serializable {
private static final long serialVersionUID = 1L;
private static final String TAG = "CLIENT_HEARTBEAT_RESPONSE";
private static final String CMD_HEARTBEAT_RESPONSE = "CR";
public static final String CMD_HEARTBEAT_RESPONSE = "CR";
private static HeartbeatResponse object = new HeartbeatResponse();
private HeartbeatResponse() {
@ -43,18 +42,9 @@ public class HeartbeatResponse implements Serializable, Protobufable {
return object;
}
@Override
public byte[] getByteArray() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
}
public String toString() {
return TAG;
}
@Override
public byte getType() {
return CIMConstant.ProtobufType.C_H_RS;
}
}

View File

@ -22,13 +22,15 @@
package com.farsunset.cim.sdk.server.model;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import com.alibaba.fastjson.JSONObject;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.feature.EncodeFormatable;
import com.farsunset.cim.sdk.server.model.proto.MessageProto;
/**
* 消息对象
*/
public class Message implements Serializable,Protobufable {
public class Message implements Serializable,EncodeFormatable {
private static final long serialVersionUID = 1L;
@ -74,6 +76,7 @@ public class Message implements Serializable,Protobufable {
private long timestamp;
public Message()
{
timestamp = System.currentTimeMillis();
@ -156,7 +159,6 @@ public class Message implements Serializable,Protobufable {
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("#Message#").append("\n");
buffer.append("mid:").append(mid).append("\n");
@ -175,7 +177,7 @@ public class Message implements Serializable,Protobufable {
return txt != null && txt.trim().length()!=0;
}
@Override
public byte[] getByteArray() {
public byte[] getProtobufBody() {
MessageProto.Model.Builder builder = MessageProto.Model.newBuilder();
builder.setMid(mid);
builder.setAction(action);
@ -200,10 +202,30 @@ public class Message implements Serializable,Protobufable {
}
return builder.build().toByteArray();
}
@Override
public byte getType() {
public byte[] getJSONBody() {
JSONObject json = new JSONObject();
json.put("contentType", getClass().getSimpleName());
json.put("mid", mid);
json.put("action", action);
json.put("title", title);
json.put("content", content);
json.put("extra", extra);
json.put("sender", sender);
json.put("receiver", receiver);
json.put("format", format);
json.put("timestamp", timestamp);
String data = json.toJSONString();
try {
return data.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
@Override
public byte getDataType() {
return CIMConstant.ProtobufType.MESSAGE;
}
}

View File

@ -22,17 +22,20 @@
package com.farsunset.cim.sdk.server.model;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import com.alibaba.fastjson.JSONObject;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.feature.EncodeFormatable;
import com.farsunset.cim.sdk.server.model.proto.ReplyBodyProto;
/**
* 请求应答对象
*
*/
public class ReplyBody implements Serializable ,Protobufable{
public class ReplyBody implements Serializable ,EncodeFormatable{
private static final long serialVersionUID = 1L;
@ -141,7 +144,7 @@ public class ReplyBody implements Serializable ,Protobufable{
return buffer.toString();
}
@Override
public byte[] getByteArray() {
public byte[] getProtobufBody() {
ReplyBodyProto.Model.Builder builder = ReplyBodyProto.Model.newBuilder();
builder.setCode(code);
if(message!=null){
@ -157,9 +160,24 @@ public class ReplyBody implements Serializable ,Protobufable{
}
@Override
public byte getType() {
// TODO Auto-generated method stub
public byte getDataType() {
return CIMConstant.ProtobufType.REPLYBODY;
}
@Override
public byte[] getJSONBody() {
JSONObject json = new JSONObject();
json.put("contentType", getClass().getSimpleName());
json.put("key", key);
json.put("code", code);
json.put("message", message);
json.put("data", data);
String data = json.toJSONString();
try {
return data.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}

View File

@ -35,7 +35,7 @@ public class SentBody implements Serializable {
private String key;
private HashMap<String, String> data = new HashMap<String, String>();
public HashMap<String, String> data = new HashMap<String, String>();
private long timestamp;

View File

@ -0,0 +1,61 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.model;
import java.io.UnsupportedEncodingException;
/**
*websocket握手响应结果
*
*/
public class WebsocketResponse{
private String token;
public WebsocketResponse(String token) {
this.token = token;
}
public byte[] getBytes() {
try {
return toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("HTTP/1.1 101 Switching Protocols");
builder.append("\r\n");
builder.append("Upgrade: websocket");
builder.append("\r\n");
builder.append("Connection: Upgrade");
builder.append("\r\n");
builder.append("Sec-WebSocket-Accept:").append(token);
builder.append("\r\n");
builder.append("\r\n");
return builder.toString();
}
}

View File

@ -19,13 +19,12 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.model;
package com.farsunset.cim.sdk.server.model.feature;
/**
* 需要向另一端发送的结构体
*/
public interface Protobufable {
byte[] getByteArray();
byte getType();
public interface EncodeFormatable {
byte[] getProtobufBody();
byte[] getJSONBody();
byte getDataType();
}

View File

@ -51,7 +51,7 @@ public class CIMSession implements Serializable{
public transient static String CHANNEL_ANDROID = "android";
public transient static String CHANNEL_WINDOWS = "windows";
public transient static String CHANNEL_WP = "wp";
public transient static String CHANNEL_BROWSER = "browser";
private transient Channel session;
private String gid;//session全局ID

View File

@ -2,11 +2,6 @@
<classpath>
<classpathentry kind="src" path="src/main/java"/>
<classpathentry kind="src" path="src/main/resource"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/jdk1.8.0_45">
<attributes>
<attribute name="owner.project.facets" value="java"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jst.server.core.container/org.eclipse.jst.server.tomcat.runtimeTarget/Apache Tomcat v7.0">
<attributes>
<attribute name="owner.project.facets" value="jst.web"/>
@ -14,5 +9,6 @@
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.web.container"/>
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.module.container"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/jdk1.8.0_131"/>
<classpathentry kind="output" path="WebContent/WEB-INF/classes"/>
</classpath>

View File

@ -3,6 +3,7 @@
<name>cim-server</name>
<comment></comment>
<projects>
<project>cim-server-sdk</project>
</projects>
<buildSpec>
<buildCommand>

View File

@ -3,6 +3,9 @@
<wb-resource deploy-path="/" source-path="/WebContent" tag="defaultRootSource"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resource"/>
<dependent-module archiveName="cim-server-sdk.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/cim-server-sdk/cim-server-sdk">
<dependency-type>uses</dependency-type>
</dependent-module>
<property name="context-root" value="cim-server"/>
<property name="java-output-path" value="/cim-server/build/classes"/>
</wb-module>

View File

@ -8,7 +8,14 @@
<div id="_main_nav" class="ui-vnav">
<ul class="ui-nav-inner">
<li style="height: 50px; text-align: center; margin-top: 10px;">
<div class="btn-group" style="margin-top: 5px;">
<a type="button" class="btn btn-danger" target="_blank"
href="javascript:openWebclient();">CIM for Web</a>
</div>
</li>
<li style="border-bottom: 1px solid #D1D6DA;"></li>
<li class="ui-item" id="sessionMenu">
<a href="<%=navBasePath%>/admin/session_list.action"> <span
class="ui-text">在线用户</span> <i class="ui-bg nav-recycle"></i> </a>

View File

@ -0,0 +1,97 @@
var CIM_URI="ws://127.0.0.1:23456";// 修改为服务器的真实IP
var CMD_HEARTBEAT_REQUEST = "SR";
var CMD_HEARTBEAT_RESPONSE = "CR";
var SDK_VERSION = "1.0.0";
var SDK_CHANNEL = "browser";
var APP_PACKAGE = "com.farsunset.webcim";
var ACCOUNT;
var socket;
var manualStop = false;
var CIMWebBridge = new Object();
CIMWebBridge.connection = function(){
manualStop = false;
socket = new WebSocket(CIM_URI);
socket.onopen = CIMWebBridge.innerOnConnectionSuccessed;
socket.onmessage = CIMWebBridge.innerOnMessageReceived;
socket.onclose = CIMWebBridge.innerOnConnectionClosed;
};
CIMWebBridge.bindAccount = function(id,deviceId){
ACCOUNT=id;
var browser = getBrowser();
var body = {};
body.key = "client_bind";
body.data = {};
body.data.account = id;
body.data.channel = SDK_CHANNEL;
body.data.version = SDK_VERSION;
body.data.osVersion = browser.version;
body.data.packageName = APP_PACKAGE;
body.data.deviceId = deviceId;
body.data.device = browser.name;
CIMWebBridge.sendRequest(body);
};
CIMWebBridge.stop = function(){
manualStop = true;
socket.close();
};
CIMWebBridge.resume = function(){
manualStop = false;
CIMWebBridge.connection();
};
CIMWebBridge.innerOnConnectionSuccessed = function(){
if(typeof onConnectionSuccessed != 'undefined' && onConnectionSuccessed instanceof Function){
onConnectionSuccessed();
}
};
CIMWebBridge.innerOnMessageReceived = function(e){
var data = e.data;
/**
* 收到服务端发来的心跳请求立即回复响应否则服务端会在10秒后断开连接
*/
if(data == CMD_HEARTBEAT_REQUEST){
socket.send(CMD_HEARTBEAT_RESPONSE);
return
}
var json = JSON.parse(data);
if(json.contentType == "Message" && typeof onMessageReceived != 'undefined' && onMessageReceived instanceof Function){
onMessageReceived(json);
}
if(json.contentType == "ReplyBody" && typeof onReplyReceived != 'undefined' && onReplyReceived instanceof Function){
onReplyReceived(json);
}
};
CIMWebBridge.innerOnConnectionClosed = function(e){
if(!manualStop){
CIMWebBridge.connection();
}
};
CIMWebBridge.sendRequest = function(body){
var json = JSON.stringify(body);
socket.send(json);
};
function getBrowser() {
var explorer = window.navigator.userAgent.toLowerCase() ;
if (explorer.indexOf("msie") >= 0) {
var ver=explorer.match(/msie ([\d.]+)/)[1];
return {name:"IE",version:ver};
}
else if (explorer.indexOf("firefox") >= 0) {
var ver=explorer.match(/firefox\/([\d.]+)/)[1];
return {name:"Firefox",version:ver};
}
else if(explorer.indexOf("chrome") >= 0){
var ver=explorer.match(/chrome\/([\d.]+)/)[1];
return {name:"Chrome",version:ver};
}
else if(explorer.indexOf("opera") >= 0){
var ver=explorer.match(/opera.([\d.]+)/)[1];
return {name:"Opera",version:ver};
}
else if(explorer.indexOf("Safari") >= 0){
var ver=explorer.match(/version\/([\d.]+)/)[1];
return {name:"Safari",version:ver};
}
return {name:"Other",version:"1.0.0"};
}

View File

@ -0,0 +1,50 @@
<%@ page language="java" pageEncoding="utf-8"%>
<%
String loginBasePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ request.getContextPath();
%>
<script type="text/javascript">
function doLogin()
{
ACCOUNT = $('#account').val();
if($.trim(ACCOUNT)=='' )
{
return;
}
showProcess('正在接入请稍后......');
CIMWebBridge.connection();
}
</script>
<div class="modal fade" id="LoginDialog" tabindex="-1" role="dialog" data-backdrop="static">
<div class="modal-dialog" style="width: 300px;margin: 30px auto;">
<div class="modal-content" >
<div class="modal-body" style="padding:0px;" >
<div style="height:150px;text-align: center; background: #5FA0D3; color: #ffffff; border: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px;">
<img src="<%=loginBasePath %>/resource/img/icon.png" style="height: 60px;width: 60px;margin-top:30px;"/>
<div style="margin-top: 20px; color: #ffffff;font-size: 16px;">使用用户帐号登录</div>
</div>
<div class="input-group" style="margin-top: 20px;margin-left:10px;margin-right:10px;margin-bottom:20px;">
<span class="input-group-addon"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></span>
<input type="text" class="form-control" id="account" maxlength="32" placeholder="帐号"
style="display: inline; width: 100%; height: 50px;" />
</div>
</div>
<div class="modal-footer" style="text-align: center;">
<a type="button" class="btn btn-success btn-lg" onclick="doLogin()"
style="width: 250px;">登录</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,70 @@
<%@ page language="java" pageEncoding="utf-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ path;
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta charset="utf-8"/>
<title>cim for web(beta) </title>
<link charset="utf-8" rel="stylesheet" href="<%=basePath%>/resource/bootstrap-3.3.6-dist/css/bootstrap.min.css" />
<link charset="utf-8" rel="stylesheet" href="<%=basePath%>/resource/css/dialog.css" />
<script type="text/javascript" src="<%=basePath%>/resource/js/jquery-2.2.3.min.js"></script>
<script type="text/javascript" src="<%=basePath%>/resource/bootstrap-3.3.6-dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="<%=basePath%>/resource/js/framework.js"></script>
<script type="text/javascript" src="cim.web.sdk.js"></script>
</head>
<script type="text/javascript">
/** 当socket连接成功回调 **/
function onConnectionSuccessed()
{
var sessionId = '<%=session.getId()%>';
CIMWebBridge.bindAccount($('#account').val(),sessionId);
}
/** 当收到请求回复时候回调 **/
function onReplyReceived(json)
{
if(json.key=='client_bind' && json.code==200)
{
hideProcess();
doHideDialog('LoginDialog');
doShowDialog('MessageDialog');
$("#current_account").text(ACCOUNT);
}
}
/** 当收到消息时候回调 **/
function onMessageReceived(message)
{
$("#messageList").append("<div class='alert alert-info' >"+message.content+"</div>");
}
$(document).ready(function(){
doShowDialog('LoginDialog');
});
</script>
<body style="background-color: rgb(190, 209, 216);width: 600px;">
<div id="global_mask" style="display: none; position: absolute; top: 0px; left: 0px; z-index: 998; background-color: rgb(190, 209, 216); opacity: 0.5; width: 100%; height: 100%; overflow: hidden; background-position: initial initial; background-repeat: initial initial;"></div>
<%@include file="loginDialog.jsp"%>
<%@include file="messageDialog.jsp"%>
</body>
</html>

View File

@ -0,0 +1,21 @@
<%@ page language="java" pageEncoding="utf-8"%>
<div class="modal fade" data-backdrop="static" id="MessageDialog" tabindex="-1" role="dialog" >
<div class="modal-dialog" style="width: 500px;margin: 30px auto;">
<div class="modal-content" >
<div class="modal-header" style="text-align: center;">
<h4>消息列表</h4>
<span style="float: left;">请在管理页面推送一条消息</span>
<span style="float: right;color: #4caf50;">当前帐号:<span id="current_account">10000</span></span>
</div>
<div class="modal-body" id="messageList" style="min-height: 600px;" >
</div>
</div>
</div>
</div>

Binary file not shown.

BIN
doc/WebSDK使用文档.doc Normal file

Binary file not shown.