移动app
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package com.lld.im.tcp;
|
||||
|
||||
import com.lld.im.codec.config.BootstrapConfig;
|
||||
import com.lld.im.tcp.reciver.MessageReciver;
|
||||
import com.lld.im.tcp.redis.RedisManager;
|
||||
import com.lld.im.tcp.register.RegistryZK;
|
||||
import com.lld.im.tcp.register.ZKit;
|
||||
import com.lld.im.tcp.server.LimServer;
|
||||
import com.lld.im.tcp.server.LimWebSocketServer;
|
||||
import com.lld.im.tcp.utils.MqFactory;
|
||||
import org.I0Itec.zkclient.ZkClient;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
public class Starter {
|
||||
public static void main(String[] args) {
|
||||
if(args.length > 0){
|
||||
start(args[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void start(String path){
|
||||
try {
|
||||
Yaml yaml = new Yaml();
|
||||
InputStream inputStream = new FileInputStream(path);
|
||||
BootstrapConfig bootstrapConfig = yaml.loadAs(inputStream, BootstrapConfig.class);
|
||||
|
||||
|
||||
new LimServer(bootstrapConfig.getLim()).start();
|
||||
new LimWebSocketServer(bootstrapConfig.getLim());
|
||||
|
||||
|
||||
RedisManager.init(bootstrapConfig);
|
||||
MqFactory.init(bootstrapConfig.getLim().getRabbitmq());
|
||||
MessageReciver.init(bootstrapConfig.getLim().getBrokerId()+"");
|
||||
|
||||
registerZK(bootstrapConfig);
|
||||
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
System.exit(500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void registerZK(BootstrapConfig config) throws UnknownHostException {
|
||||
String hostAddress = InetAddress.getLocalHost().getHostAddress();
|
||||
ZkClient zkClient = new ZkClient(config.getLim().getZkConfig().getZkAddr(),
|
||||
config.getLim().getZkConfig().getZkConnectTimeOut());
|
||||
ZKit zKit = new ZKit(zkClient);
|
||||
RegistryZK registryZK = new RegistryZK(zKit, hostAddress, config.getLim());
|
||||
Thread thread = new Thread(registryZK);
|
||||
thread.start();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.lld.im.tcp.feign;
|
||||
|
||||
import com.lld.im.common.ResponseVO;
|
||||
import com.lld.im.common.model.message.CheckSendMessageReq;
|
||||
import feign.Headers;
|
||||
import feign.RequestLine;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @author: lld
|
||||
* @version: 1.0
|
||||
*/
|
||||
public interface FeignMessageService {
|
||||
|
||||
@Headers({"Content-Type: application/json","Accept: application/json"})
|
||||
@RequestLine("POST /message/checkSend")
|
||||
public ResponseVO checkSendMessage(CheckSendMessageReq o);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.lld.im.tcp.handler;
|
||||
|
||||
import com.lld.im.common.constant.Constants;
|
||||
import com.lld.im.tcp.utils.SessionSocketHolder;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.util.AttributeKey;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private Long heartBeatTime;
|
||||
|
||||
public HeartBeatHandler(Long heartBeatTime) {
|
||||
this.heartBeatTime = heartBeatTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
|
||||
// 判断evt是否是IdleStateEvent(用于触发用户事件,包含 读空闲/写空闲/读写空闲 )
|
||||
if (evt instanceof IdleStateEvent) {
|
||||
IdleStateEvent event = (IdleStateEvent)evt; // 强制类型转换
|
||||
if (event.state() == IdleState.READER_IDLE) {
|
||||
log.info("读空闲");
|
||||
} else if (event.state() == IdleState.WRITER_IDLE) {
|
||||
log.info("进入写空闲");
|
||||
} else if (event.state() == IdleState.ALL_IDLE) {
|
||||
Long lastReadTime = (Long) ctx.channel()
|
||||
.attr(AttributeKey.valueOf(Constants.ReadTime)).get();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if(lastReadTime != null && now - lastReadTime > heartBeatTime){
|
||||
//TODO 退后台逻辑
|
||||
SessionSocketHolder.offlineUserSession((NioSocketChannel) ctx.channel());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package com.lld.im.tcp.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
import com.lld.im.codec.pack.LoginPack;
|
||||
import com.lld.im.codec.pack.message.ChatMessageAck;
|
||||
import com.lld.im.codec.pack.user.LoginAckPack;
|
||||
import com.lld.im.codec.pack.user.UserStatusChangeNotifyPack;
|
||||
import com.lld.im.codec.proto.Message;
|
||||
import com.lld.im.codec.proto.MessagePack;
|
||||
import com.lld.im.common.ResponseVO;
|
||||
import com.lld.im.common.constant.Constants;
|
||||
import com.lld.im.common.enums.ImConnectStatusEnum;
|
||||
import com.lld.im.common.enums.command.GroupEventCommand;
|
||||
import com.lld.im.common.enums.command.MessageCommand;
|
||||
import com.lld.im.common.enums.command.SystemCommand;
|
||||
import com.lld.im.common.enums.command.UserEventCommand;
|
||||
import com.lld.im.common.model.UserClientDto;
|
||||
import com.lld.im.common.model.UserSession;
|
||||
import com.lld.im.common.model.message.CheckSendMessageReq;
|
||||
import com.lld.im.tcp.feign.FeignMessageService;
|
||||
import com.lld.im.tcp.publish.MqMessageProducer;
|
||||
import com.lld.im.tcp.redis.RedisManager;
|
||||
import com.lld.im.tcp.utils.SessionSocketHolder;
|
||||
import feign.jackson.JacksonDecoder;
|
||||
import feign.jackson.JacksonEncoder;
|
||||
import feign.Feign;
|
||||
import feign.Request;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.redisson.api.RMap;
|
||||
import org.redisson.api.RTopic;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
public class NettyServerHandler extends SimpleChannelInboundHandler<Message> {
|
||||
private final static Logger logger = LoggerFactory.getLogger(NettyServerHandler.class);
|
||||
|
||||
private Integer brokerId;
|
||||
|
||||
|
||||
private FeignMessageService feignMessageService;
|
||||
|
||||
|
||||
public NettyServerHandler(Integer brokerId, String logicUrl) {
|
||||
this.brokerId = brokerId;
|
||||
|
||||
feignMessageService = Feign.builder()
|
||||
.encoder(new JacksonEncoder())
|
||||
.decoder(new JacksonDecoder())
|
||||
.options(new Request.Options(1000, 3500))//设置超时时间
|
||||
.target(FeignMessageService.class, logicUrl);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {
|
||||
logger.info("***********************收到消息:{}", msg);
|
||||
logger.info("***********************收到消息:{}", msg);
|
||||
logger.info("***********************收到消息:{}", msg);
|
||||
|
||||
Integer command = msg.getMessageHeader().getCommand();
|
||||
|
||||
//登录command
|
||||
if (command == SystemCommand.LOGIN.getCommand()) {
|
||||
|
||||
LoginPack loginPack = JSON.parseObject(JSONObject.toJSONString(msg.getMessagePack()),
|
||||
new TypeReference<LoginPack>() {
|
||||
}.getType());
|
||||
/** 登陸事件 **/
|
||||
String userId = loginPack.getUserId();
|
||||
/** 为channel设置用户id **/
|
||||
ctx.channel().attr(AttributeKey.valueOf(Constants.UserId)).set(userId);
|
||||
String clientImei = msg.getMessageHeader().getClientType() + ":" + msg.getMessageHeader().getImei();
|
||||
/** 为channel设置client和imel **/
|
||||
ctx.channel().attr(AttributeKey.valueOf(Constants.ClientImei)).set(clientImei);
|
||||
/** 为channel设置appId **/
|
||||
ctx.channel().attr(AttributeKey.valueOf(Constants.AppId)).set(msg.getMessageHeader().getAppId());
|
||||
/** 为channel设置ClientType **/
|
||||
ctx.channel().attr(AttributeKey.valueOf(Constants.ClientType))
|
||||
.set(msg.getMessageHeader().getClientType());
|
||||
/** 为channel设置Imei **/
|
||||
ctx.channel().attr(AttributeKey.valueOf(Constants.Imei))
|
||||
.set(msg.getMessageHeader().getImei());
|
||||
|
||||
|
||||
UserSession userSession = new UserSession();
|
||||
userSession.setAppId(msg.getMessageHeader().getAppId());
|
||||
userSession.setClientType(msg.getMessageHeader().getClientType());
|
||||
userSession.setUserId(loginPack.getUserId());
|
||||
userSession.setConnectState(ImConnectStatusEnum.ONLINE_STATUS.getCode());
|
||||
userSession.setBrokerId(brokerId);
|
||||
userSession.setImei(msg.getMessageHeader().getImei());
|
||||
|
||||
try {
|
||||
InetAddress localHost = InetAddress.getLocalHost();
|
||||
userSession.setBrokerHost(localHost.getHostAddress());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
RedissonClient redissonClient = RedisManager.getRedissonClient();
|
||||
RMap<String, String> map = redissonClient.getMap(msg.getMessageHeader().getAppId() + Constants.RedisConstants.UserSessionConstants + loginPack.getUserId());
|
||||
map.put(msg.getMessageHeader().getClientType() + ":" + msg.getMessageHeader().getImei()
|
||||
, JSONObject.toJSONString(userSession));
|
||||
SessionSocketHolder
|
||||
.put(msg.getMessageHeader().getAppId()
|
||||
, loginPack.getUserId(),
|
||||
msg.getMessageHeader().getClientType(), msg.getMessageHeader().getImei(), (NioSocketChannel) ctx.channel());
|
||||
|
||||
|
||||
UserClientDto dto = new UserClientDto();
|
||||
dto.setImei(msg.getMessageHeader().getImei());
|
||||
dto.setUserId(loginPack.getUserId());
|
||||
dto.setClientType(msg.getMessageHeader().getClientType());
|
||||
dto.setAppId(msg.getMessageHeader().getAppId());
|
||||
|
||||
RTopic topic = redissonClient.getTopic(Constants.RedisConstants.UserLoginChannel);
|
||||
topic.publish(JSONObject.toJSONString(dto));
|
||||
|
||||
UserStatusChangeNotifyPack userStatusChangeNotifyPack = new UserStatusChangeNotifyPack();
|
||||
userStatusChangeNotifyPack.setAppId(msg.getMessageHeader().getAppId());
|
||||
userStatusChangeNotifyPack.setUserId(loginPack.getUserId());
|
||||
userStatusChangeNotifyPack.setStatus(ImConnectStatusEnum.ONLINE_STATUS.getCode());
|
||||
MqMessageProducer.sendMessage(userStatusChangeNotifyPack,msg.getMessageHeader(), UserEventCommand.USER_ONLINE_STATUS_CHANGE.getCommand());
|
||||
|
||||
MessagePack<LoginAckPack> loginSuccess = new MessagePack<>();
|
||||
LoginAckPack loginAckPack = new LoginAckPack();
|
||||
loginAckPack.setUserId(loginPack.getUserId());
|
||||
loginSuccess.setCommand(SystemCommand.LOGINACK.getCommand());
|
||||
loginSuccess.setData(loginAckPack);
|
||||
loginSuccess.setImei(msg.getMessageHeader().getImei());
|
||||
loginSuccess.setAppId(msg.getMessageHeader().getAppId());
|
||||
ctx.channel().writeAndFlush(loginSuccess);
|
||||
logger.info("===============登录成功:{}",loginPack.getUserId());
|
||||
|
||||
}else if(command == SystemCommand.LOGOUT.getCommand()){
|
||||
//删除session
|
||||
//redis 删除
|
||||
SessionSocketHolder.removeUserSession((NioSocketChannel) ctx.channel());
|
||||
}else if(command == SystemCommand.PING.getCommand()){
|
||||
ctx.channel().attr(AttributeKey.valueOf(Constants.ReadTime)).set(System.currentTimeMillis());
|
||||
}else if(command == MessageCommand.MSG_P2P.getCommand()
|
||||
|| command == GroupEventCommand.MSG_GROUP.getCommand()){
|
||||
try {
|
||||
String toId = "";
|
||||
CheckSendMessageReq req = new CheckSendMessageReq();
|
||||
req.setAppId(msg.getMessageHeader().getAppId());
|
||||
req.setCommand(msg.getMessageHeader().getCommand());
|
||||
JSONObject jsonObject = JSON.parseObject(JSONObject.toJSONString(msg.getMessagePack()));
|
||||
String fromId = jsonObject.getString("fromId");
|
||||
if(command == MessageCommand.MSG_P2P.getCommand()){
|
||||
toId = jsonObject.getString("toId");
|
||||
}else {
|
||||
toId = jsonObject.getString("groupId");
|
||||
}
|
||||
req.setToId(toId);
|
||||
req.setFromId(fromId);
|
||||
|
||||
ResponseVO responseVO = feignMessageService.checkSendMessage(req);
|
||||
if(responseVO.isOk()){
|
||||
MqMessageProducer.sendMessage(msg,command);
|
||||
}else{
|
||||
Integer ackCommand = 0;
|
||||
if(command == MessageCommand.MSG_P2P.getCommand()){
|
||||
ackCommand = MessageCommand.MSG_ACK.getCommand();
|
||||
}else {
|
||||
ackCommand = GroupEventCommand.GROUP_MSG_ACK.getCommand();
|
||||
}
|
||||
|
||||
ChatMessageAck chatMessageAck = new ChatMessageAck(jsonObject.getString("messageId"));
|
||||
responseVO.setData(chatMessageAck);
|
||||
MessagePack<ResponseVO> ack = new MessagePack<>();
|
||||
ack.setData(responseVO);
|
||||
ack.setCommand(ackCommand);
|
||||
ctx.channel().writeAndFlush(ack);
|
||||
}
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}else {
|
||||
MqMessageProducer.sendMessage(msg,command);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
|
||||
package com.lld.im.tcp.publish;
|
||||
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.lld.im.codec.proto.Message;
|
||||
import com.lld.im.codec.proto.MessageHeader;
|
||||
import com.lld.im.common.constant.Constants;
|
||||
import com.lld.im.common.enums.command.CommandType;
|
||||
import com.lld.im.tcp.utils.MqFactory;
|
||||
import com.rabbitmq.client.Channel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @author: lld
|
||||
* @version: 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class MqMessageProducer {
|
||||
|
||||
public static void sendMessage(Message message,Integer command){
|
||||
Channel channel = null;
|
||||
String com = command.toString();
|
||||
String commandSub = com.substring(0, 1);
|
||||
CommandType commandType = CommandType.getCommandType(commandSub);
|
||||
String channelName = "";
|
||||
if(commandType == CommandType.MESSAGE){
|
||||
channelName = Constants.RabbitConstants.Im2MessageService;
|
||||
}else if(commandType == CommandType.GROUP){
|
||||
channelName = Constants.RabbitConstants.Im2GroupService;
|
||||
}else if(commandType == CommandType.FRIEND){
|
||||
channelName = Constants.RabbitConstants.Im2FriendshipService;
|
||||
}else if(commandType == CommandType.USER){
|
||||
channelName = Constants.RabbitConstants.Im2UserService;
|
||||
}
|
||||
|
||||
try {
|
||||
channel = MqFactory.getChannel(channelName);
|
||||
|
||||
JSONObject o = (JSONObject) JSON.toJSON(message.getMessagePack());
|
||||
o.put("command",command);
|
||||
o.put("clientType",message.getMessageHeader().getClientType());
|
||||
o.put("imei",message.getMessageHeader().getImei());
|
||||
o.put("appId",message.getMessageHeader().getAppId());
|
||||
channel.basicPublish(channelName,"",
|
||||
null, o.toJSONString().getBytes());
|
||||
}catch (Exception e){
|
||||
log.error("发送消息出现异常:{}",e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendMessage(Object message, MessageHeader header, Integer command){
|
||||
Channel channel = null;
|
||||
String com = command.toString();
|
||||
String commandSub = com.substring(0, 1);
|
||||
CommandType commandType = CommandType.getCommandType(commandSub);
|
||||
String channelName = "";
|
||||
if(commandType == CommandType.MESSAGE){
|
||||
channelName = Constants.RabbitConstants.Im2MessageService;
|
||||
}else if(commandType == CommandType.GROUP){
|
||||
channelName = Constants.RabbitConstants.Im2GroupService;
|
||||
}else if(commandType == CommandType.FRIEND){
|
||||
channelName = Constants.RabbitConstants.Im2FriendshipService;
|
||||
}else if(commandType == CommandType.USER){
|
||||
channelName = Constants.RabbitConstants.Im2UserService;
|
||||
}
|
||||
|
||||
try {
|
||||
channel = MqFactory.getChannel(channelName);
|
||||
|
||||
JSONObject o = (JSONObject) JSON.toJSON(message);
|
||||
o.put("command",command);
|
||||
o.put("clientType",header.getClientType());
|
||||
o.put("imei",header.getImei());
|
||||
o.put("appId",header.getAppId());
|
||||
channel.basicPublish(channelName,"",
|
||||
null, o.toJSONString().getBytes());
|
||||
}catch (Exception e){
|
||||
log.error("发送消息出现异常:{}",e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.lld.im.tcp.reciver;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.lld.im.codec.proto.MessagePack;
|
||||
import com.lld.im.common.constant.Constants;
|
||||
import com.lld.im.tcp.reciver.process.BaseProcess;
|
||||
import com.lld.im.tcp.reciver.process.ProcessFactory;
|
||||
import com.lld.im.tcp.utils.MqFactory;
|
||||
import com.rabbitmq.client.AMQP;
|
||||
import com.rabbitmq.client.Channel;
|
||||
import com.rabbitmq.client.DefaultConsumer;
|
||||
import com.rabbitmq.client.Envelope;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @author: rowger
|
||||
* @version: 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class MessageReciver {
|
||||
|
||||
private static String brokerId;
|
||||
|
||||
private static void startReciverMessage() {
|
||||
try {
|
||||
String channelNameMessageService2Im=Constants.RabbitConstants.MessageService2Im + brokerId;
|
||||
|
||||
log.info("==============channelNameMessageService2Im:"+channelNameMessageService2Im);
|
||||
|
||||
Channel channel = MqFactory.getChannel(channelNameMessageService2Im);
|
||||
channel.queueDeclare(channelNameMessageService2Im,true, false, false, null);
|
||||
channel.queueBind(channelNameMessageService2Im,Constants.RabbitConstants.MessageService2Im, brokerId);
|
||||
|
||||
channel.basicConsume(channelNameMessageService2Im, false,new DefaultConsumer(channel) {
|
||||
@Override
|
||||
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
|
||||
try {
|
||||
//处理消息服务发来的消息
|
||||
String msgStr = new String(body);
|
||||
log.info("=================处理消息服务发来的消息:{msgStr}:"+msgStr);
|
||||
MessagePack messagePack =
|
||||
JSONObject.parseObject(msgStr, MessagePack.class);
|
||||
BaseProcess messageProcess = ProcessFactory
|
||||
.getMessageProcess(messagePack.getCommand());
|
||||
messageProcess.process(messagePack);
|
||||
|
||||
channel.basicAck(envelope.getDeliveryTag(),false);
|
||||
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
channel.basicNack(envelope.getDeliveryTag(),false,false);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
startReciverMessage();
|
||||
log.info("===========startReciverMessage no brokerId");
|
||||
|
||||
}
|
||||
|
||||
public static void init(String brokerId) {
|
||||
|
||||
if (StringUtils.isBlank(MessageReciver.brokerId)) {
|
||||
MessageReciver.brokerId = brokerId;
|
||||
}
|
||||
startReciverMessage();
|
||||
log.info("===========startReciverMessage has brokerId:"+brokerId);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.lld.im.tcp.reciver;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.lld.im.codec.proto.MessagePack;
|
||||
import com.lld.im.common.ClientType;
|
||||
import com.lld.im.common.constant.Constants;
|
||||
import com.lld.im.common.enums.DeviceMultiLoginEnum;
|
||||
import com.lld.im.common.enums.command.SystemCommand;
|
||||
import com.lld.im.common.model.UserClientDto;
|
||||
import com.lld.im.tcp.handler.NettyServerHandler;
|
||||
import com.lld.im.tcp.redis.RedisManager;
|
||||
import com.lld.im.tcp.utils.SessionSocketHolder;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.redisson.api.RTopic;
|
||||
import org.redisson.api.listener.MessageListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* 多端同步:1单端登录:一端在线:踢掉除了本clinetType + imel 的设备
|
||||
* 2双端登录:允许pc/mobile 其中一端登录 + web端 踢掉除了本clinetType + imel 以外的web端设备
|
||||
* 3 三端登录:允许手机+pc+web,踢掉同端的其他imei 除了web
|
||||
* 4 不做任何处理
|
||||
*
|
||||
* @author: rowger
|
||||
* @version: 1.0
|
||||
*/
|
||||
public class UserLoginMessageListener {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(UserLoginMessageListener.class);
|
||||
|
||||
private Integer loginModel;
|
||||
|
||||
public UserLoginMessageListener(Integer loginModel) {
|
||||
this.loginModel = loginModel;
|
||||
}
|
||||
|
||||
public void listenerUserLogin(){
|
||||
RTopic topic = RedisManager.getRedissonClient().getTopic(Constants.RedisConstants.UserLoginChannel);
|
||||
topic.addListener(String.class, new MessageListener<String>() {
|
||||
@Override
|
||||
public void onMessage(CharSequence charSequence, String msg) {
|
||||
logger.info("收到用户上线通知:" + msg);
|
||||
UserClientDto dto = JSONObject.parseObject(msg, UserClientDto.class);
|
||||
List<NioSocketChannel> nioSocketChannels = SessionSocketHolder.get(dto.getAppId(), dto.getUserId());
|
||||
|
||||
for (NioSocketChannel nioSocketChannel : nioSocketChannels) {
|
||||
if(loginModel == DeviceMultiLoginEnum.ONE.getLoginMode()){
|
||||
Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get();
|
||||
String imei = (String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.Imei)).get();
|
||||
|
||||
if(!(clientType + ":" + imei).equals(dto.getClientType()+":"+dto.getImei())){
|
||||
MessagePack<Object> pack = new MessagePack<>();
|
||||
pack.setToId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
|
||||
pack.setUserId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
|
||||
pack.setCommand(SystemCommand.MUTUALLOGIN.getCommand());
|
||||
nioSocketChannel.writeAndFlush(pack);
|
||||
}
|
||||
|
||||
}else if(loginModel == DeviceMultiLoginEnum.TWO.getLoginMode()){
|
||||
if(dto.getClientType() == ClientType.WEB.getCode()){
|
||||
continue;
|
||||
}
|
||||
Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get();
|
||||
|
||||
if (clientType == ClientType.WEB.getCode()){
|
||||
continue;
|
||||
}
|
||||
String imei = (String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.Imei)).get();
|
||||
if(!(clientType + ":" + imei).equals(dto.getClientType()+":"+dto.getImei())){
|
||||
MessagePack<Object> pack = new MessagePack<>();
|
||||
pack.setToId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
|
||||
pack.setUserId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
|
||||
pack.setCommand(SystemCommand.MUTUALLOGIN.getCommand());
|
||||
nioSocketChannel.writeAndFlush(pack);
|
||||
}
|
||||
|
||||
}else if(loginModel == DeviceMultiLoginEnum.THREE.getLoginMode()){
|
||||
|
||||
Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get();
|
||||
String imei = (String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.Imei)).get();
|
||||
if(dto.getClientType() == ClientType.WEB.getCode()){
|
||||
continue;
|
||||
}
|
||||
|
||||
Boolean isSameClient = false;
|
||||
if((clientType == ClientType.IOS.getCode() ||
|
||||
clientType == ClientType.ANDROID.getCode()) &&
|
||||
(dto.getClientType() == ClientType.IOS.getCode() ||
|
||||
dto.getClientType() == ClientType.ANDROID.getCode())){
|
||||
isSameClient = true;
|
||||
}
|
||||
|
||||
if((clientType == ClientType.MAC.getCode() ||
|
||||
clientType == ClientType.WINDOWS.getCode()) &&
|
||||
(dto.getClientType() == ClientType.MAC.getCode() ||
|
||||
dto.getClientType() == ClientType.WINDOWS.getCode())){
|
||||
isSameClient = true;
|
||||
}
|
||||
|
||||
if(isSameClient && !(clientType + ":" + imei).equals(dto.getClientType()+":"+dto.getImei())){
|
||||
MessagePack<Object> pack = new MessagePack<>();
|
||||
pack.setToId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
|
||||
pack.setUserId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
|
||||
pack.setCommand(SystemCommand.MUTUALLOGIN.getCommand());
|
||||
nioSocketChannel.writeAndFlush(pack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.lld.im.tcp.reciver.process;
|
||||
|
||||
import com.lld.im.codec.proto.MessagePack;
|
||||
import com.lld.im.tcp.utils.SessionSocketHolder;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @author: rowger
|
||||
* @version: 1.0
|
||||
*/
|
||||
public abstract class BaseProcess {
|
||||
|
||||
public abstract void processBefore();
|
||||
|
||||
public void process(MessagePack messagePack){
|
||||
processBefore();
|
||||
NioSocketChannel channel = SessionSocketHolder.get(messagePack.getAppId(),
|
||||
messagePack.getToId(), messagePack.getClientType(),
|
||||
messagePack.getImei());
|
||||
if(channel != null){
|
||||
channel.writeAndFlush(messagePack);
|
||||
}
|
||||
processAfter();
|
||||
}
|
||||
|
||||
public abstract void processAfter();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.lld.im.tcp.reciver.process;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @author: rowger
|
||||
* @version: 1.0
|
||||
*/
|
||||
public class ProcessFactory {
|
||||
|
||||
private static BaseProcess defaultProcess;
|
||||
|
||||
static {
|
||||
defaultProcess = new BaseProcess() {
|
||||
@Override
|
||||
public void processBefore() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processAfter() {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static BaseProcess getMessageProcess(Integer command) {
|
||||
return defaultProcess;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.lld.im.tcp.redis;
|
||||
|
||||
import com.lld.im.codec.config.BootstrapConfig;
|
||||
import com.sun.org.apache.regexp.internal.RE;
|
||||
import org.redisson.api.RedissonClient;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @author: rowger
|
||||
* @version: 1.0
|
||||
*/
|
||||
public class RedisManager {
|
||||
|
||||
private static RedissonClient redissonClient;
|
||||
|
||||
private static Integer loginModel;
|
||||
|
||||
public static void init(BootstrapConfig config){
|
||||
loginModel = config.getLim().getLoginModel();
|
||||
SingleClientStrategy singleClientStrategy = new SingleClientStrategy();
|
||||
redissonClient = singleClientStrategy.getRedissonClient(config.getLim().getRedis());
|
||||
// UserLoginMessageListener userLoginMessageListener = new UserLoginMessageListener(loginModel);
|
||||
// userLoginMessageListener.listenerUserLogin();
|
||||
}
|
||||
|
||||
public static RedissonClient getRedissonClient(){
|
||||
return redissonClient;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.lld.im.tcp.redis;
|
||||
|
||||
|
||||
import com.lld.im.codec.config.BootstrapConfig;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.client.codec.StringCodec;
|
||||
import org.redisson.config.Config;
|
||||
import org.redisson.config.SingleServerConfig;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @author: rowger
|
||||
* @version: 1.0
|
||||
*/
|
||||
public class SingleClientStrategy {
|
||||
|
||||
public RedissonClient getRedissonClient(BootstrapConfig.RedisConfig redisConfig) {
|
||||
Config config = new Config();
|
||||
String node = redisConfig.getSingle().getAddress();
|
||||
node = node.startsWith("redis://") ? node : "redis://" + node;
|
||||
SingleServerConfig serverConfig = config.useSingleServer()
|
||||
.setAddress(node)
|
||||
.setDatabase(redisConfig.getDatabase())
|
||||
.setTimeout(redisConfig.getTimeout())
|
||||
.setConnectionMinimumIdleSize(redisConfig.getPoolMinIdle())
|
||||
.setConnectTimeout(redisConfig.getPoolConnTimeout())
|
||||
.setConnectionPoolSize(redisConfig.getPoolSize());
|
||||
if (StringUtils.isNotBlank(redisConfig.getPassword())) {
|
||||
serverConfig.setPassword(redisConfig.getPassword());
|
||||
}
|
||||
StringCodec stringCodec = new StringCodec();
|
||||
config.setCodec(stringCodec);
|
||||
return Redisson.create(config);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.lld.im.tcp.register;
|
||||
|
||||
import com.lld.im.codec.config.BootstrapConfig;
|
||||
import com.lld.im.common.constant.Constants;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @author: rowger
|
||||
* @version: 1.0
|
||||
*/
|
||||
public class RegistryZK implements Runnable {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(RegistryZK.class);
|
||||
|
||||
private ZKit zKit;
|
||||
|
||||
private String ip;
|
||||
|
||||
private BootstrapConfig.TcpConfig tcpConfig;
|
||||
|
||||
public RegistryZK(ZKit zKit, String ip, BootstrapConfig.TcpConfig tcpConfig) {
|
||||
this.zKit = zKit;
|
||||
this.ip = ip;
|
||||
this.tcpConfig = tcpConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
zKit.createRootNode();
|
||||
String tcpPath = Constants.ImCoreZkRoot + Constants.ImCoreZkRootTcp + "/" + ip + ":" + tcpConfig.getTcpPort();
|
||||
zKit.createNode(tcpPath);
|
||||
logger.info("Registry zookeeper tcpPath success, msg=[{}]", tcpPath);
|
||||
|
||||
String webPath =
|
||||
Constants.ImCoreZkRoot + Constants.ImCoreZkRootWeb + "/" + ip + ":" + tcpConfig.getWebSocketPort();
|
||||
zKit.createNode(webPath);
|
||||
logger.info("Registry zookeeper webPath success, msg=[{}]", tcpPath);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.lld.im.tcp.register;
|
||||
|
||||
import com.lld.im.common.constant.Constants;
|
||||
import org.I0Itec.zkclient.ZkClient;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @author: rowger
|
||||
* @version: 1.0
|
||||
*/
|
||||
public class ZKit {
|
||||
|
||||
private ZkClient zkClient;
|
||||
|
||||
public ZKit(ZkClient zkClient) {
|
||||
this.zkClient = zkClient;
|
||||
}
|
||||
|
||||
//im-coreRoot/tcp/ip:port
|
||||
public void createRootNode(){
|
||||
boolean exists = zkClient.exists(Constants.ImCoreZkRoot);
|
||||
if(!exists){
|
||||
zkClient.createPersistent(Constants.ImCoreZkRoot);
|
||||
}
|
||||
boolean tcpExists = zkClient.exists(Constants.ImCoreZkRoot +
|
||||
Constants.ImCoreZkRootTcp);
|
||||
if(!tcpExists){
|
||||
zkClient.createPersistent(Constants.ImCoreZkRoot +
|
||||
Constants.ImCoreZkRootTcp);
|
||||
}
|
||||
|
||||
boolean webExists = zkClient.exists(Constants.ImCoreZkRoot +
|
||||
Constants.ImCoreZkRootWeb);
|
||||
if(!tcpExists){
|
||||
zkClient.createPersistent(Constants.ImCoreZkRoot +
|
||||
Constants.ImCoreZkRootWeb);
|
||||
}
|
||||
}
|
||||
|
||||
//ip+port
|
||||
public void createNode(String path){
|
||||
if(!zkClient.exists(path)){
|
||||
zkClient.createPersistent(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.lld.im.tcp.server;
|
||||
|
||||
import com.lld.im.codec.MessageDecoder;
|
||||
import com.lld.im.codec.config.BootstrapConfig;
|
||||
import com.lld.im.tcp.handler.HeartBeatHandler;
|
||||
import com.lld.im.tcp.handler.NettyServerHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
|
||||
public class LimServer {
|
||||
private final static Logger logger = LoggerFactory.getLogger(LimServer.class);
|
||||
|
||||
BootstrapConfig.TcpConfig config;
|
||||
EventLoopGroup mainGroup;
|
||||
EventLoopGroup subGroup;
|
||||
ServerBootstrap server;
|
||||
|
||||
public LimServer(BootstrapConfig.TcpConfig config) {
|
||||
this.config = config;
|
||||
|
||||
mainGroup = new NioEventLoopGroup();
|
||||
subGroup = new NioEventLoopGroup();
|
||||
|
||||
server = new ServerBootstrap();
|
||||
|
||||
server.group(mainGroup, subGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG, 10240) // 服务端可连接队列大小
|
||||
.option(ChannelOption.SO_REUSEADDR, true) // 参数表示允许重复使用本地地址和端口
|
||||
.childOption(ChannelOption.TCP_NODELAY, true) // 是否禁用Nagle算法 简单点说是否批量发送数据 true关闭 false开启。 开启的话可以减少一定的网络开销,但影响消息实时性
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, true) // 保活开关2h没有数据服务端会发送心跳包
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) throws Exception {
|
||||
System.out.println("============================");
|
||||
ch.pipeline().addLast(new MessageDecoder());
|
||||
ch.pipeline().addLast(new HeartBeatHandler(config.getHeartBeatTime()));
|
||||
ch.pipeline().addLast(new NettyServerHandler(config.getBrokerId(),config.getLogicUrl()));
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// server.bind(config.getTcpPort());
|
||||
}
|
||||
|
||||
public void start(){
|
||||
this.server.bind(this.config.getTcpPort());
|
||||
logger.info("==================tcp server start on {}",this.config.getTcpPort());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.lld.im.tcp.server;
|
||||
|
||||
import com.lld.im.codec.WebSocketMessageDecoder;
|
||||
import com.lld.im.codec.WebSocketMessageEncoder;
|
||||
import com.lld.im.codec.config.BootstrapConfig;
|
||||
import com.lld.im.tcp.handler.NettyServerHandler;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class LimWebSocketServer {
|
||||
private final static Logger logger = LoggerFactory.getLogger(LimWebSocketServer.class);
|
||||
|
||||
public LimWebSocketServer(BootstrapConfig.TcpConfig config) {
|
||||
EventLoopGroup mainGroup = new NioEventLoopGroup();
|
||||
EventLoopGroup subGroup = new NioEventLoopGroup();
|
||||
ServerBootstrap serverBootstrap = new ServerBootstrap();
|
||||
serverBootstrap.group(mainGroup, subGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG, 10240) // 服务端可连接队列大小
|
||||
.option(ChannelOption.SO_REUSEADDR, true) // 参数表示允许重复使用本地地址和端口
|
||||
.childOption(ChannelOption.TCP_NODELAY, true) // 是否禁用Nagle算法 简单点说是否批量发送数据 true关闭 false开启。 开启的话可以减少一定的网络开销,但影响消息实时性
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, true) // 保活开关2h没有数据服务端会发送心跳包
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) throws Exception {
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
// websocket 基于http协议,所以要有http编解码器
|
||||
pipeline.addLast("http-codec", new HttpServerCodec());
|
||||
// 对写大数据流的支持
|
||||
pipeline.addLast("http-chunked", new ChunkedWriteHandler());
|
||||
// 几乎在netty中的编程,都会使用到此hanler
|
||||
pipeline.addLast("aggregator", new HttpObjectAggregator(65535));
|
||||
/**
|
||||
* websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : /ws
|
||||
* 本handler会帮你处理一些繁重的复杂的事
|
||||
* 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳
|
||||
* 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
|
||||
*/
|
||||
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
|
||||
pipeline.addLast(new WebSocketMessageDecoder());
|
||||
pipeline.addLast(new WebSocketMessageEncoder());
|
||||
pipeline.addLast(new NettyServerHandler(config.getBrokerId(),config.getLogicUrl()));
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
serverBootstrap.bind(config.getWebSocketPort());
|
||||
logger.info("websocket start====={}",config.getWebSocketPort());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.lld.im.tcp.utils;
|
||||
|
||||
import com.lld.im.codec.config.BootstrapConfig;
|
||||
import com.rabbitmq.client.Channel;
|
||||
import com.rabbitmq.client.Connection;
|
||||
import com.rabbitmq.client.ConnectionFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @author: rowger
|
||||
* @version: 1.0
|
||||
*/
|
||||
public class MqFactory {
|
||||
|
||||
private static ConnectionFactory factory = null;
|
||||
|
||||
private static Channel defaultChannel;
|
||||
|
||||
private static ConcurrentHashMap<String,Channel> channelMap = new ConcurrentHashMap<>();
|
||||
|
||||
private static Connection getConnection() throws IOException, TimeoutException {
|
||||
Connection connection = factory.newConnection();
|
||||
return connection;
|
||||
}
|
||||
|
||||
public static Channel getChannel(String channelName) throws IOException, TimeoutException {
|
||||
Channel channel = channelMap.get(channelName);
|
||||
if(channel == null){
|
||||
channel = getConnection().createChannel();
|
||||
channelMap.put(channelName,channel);
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
public static void init(BootstrapConfig.Rabbitmq rabbitmq){
|
||||
if(factory == null){
|
||||
factory = new ConnectionFactory();
|
||||
factory.setHost(rabbitmq.getHost());
|
||||
factory.setPort(rabbitmq.getPort());
|
||||
factory.setUsername(rabbitmq.getUserName());
|
||||
factory.setPassword(rabbitmq.getPassword());
|
||||
factory.setVirtualHost(rabbitmq.getVirtualHost());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.lld.im.tcp.utils;
|
||||
|
||||
import com.lld.im.codec.proto.MessageHeader;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.lld.im.codec.pack.user.UserStatusChangeNotifyPack;
|
||||
import com.lld.im.common.constant.Constants;
|
||||
import com.lld.im.common.enums.ImConnectStatusEnum;
|
||||
import com.lld.im.common.enums.command.UserEventCommand;
|
||||
import com.lld.im.common.model.UserClientDto;
|
||||
import com.lld.im.common.model.UserSession;
|
||||
import com.lld.im.tcp.publish.MqMessageProducer;
|
||||
import com.lld.im.tcp.redis.RedisManager;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.redisson.api.RMap;
|
||||
import org.redisson.api.RedissonClient;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class SessionSocketHolder {
|
||||
|
||||
private static final Map<UserClientDto, NioSocketChannel> CHANNELS = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static void put(Integer appId,String userId,Integer clientType,
|
||||
String imei
|
||||
,NioSocketChannel channel){
|
||||
UserClientDto dto = new UserClientDto();
|
||||
dto.setImei(imei);
|
||||
dto.setAppId(appId);
|
||||
dto.setClientType(clientType);
|
||||
dto.setUserId(userId);
|
||||
CHANNELS.put(dto,channel);
|
||||
}
|
||||
|
||||
public static NioSocketChannel get(Integer appId,String userId,
|
||||
Integer clientType,String imei){
|
||||
UserClientDto dto = new UserClientDto();
|
||||
dto.setImei(imei);
|
||||
dto.setAppId(appId);
|
||||
dto.setClientType(clientType);
|
||||
dto.setUserId(userId);
|
||||
return CHANNELS.get(dto);
|
||||
}
|
||||
|
||||
public static List<NioSocketChannel> get(Integer appId , String id) {
|
||||
|
||||
Set<UserClientDto> channelInfos = CHANNELS.keySet();
|
||||
List<NioSocketChannel> channels = new ArrayList<>();
|
||||
|
||||
channelInfos.forEach(channel ->{
|
||||
if(channel.getAppId().equals(appId) && id.equals(channel.getUserId())){
|
||||
channels.add(CHANNELS.get(channel));
|
||||
}
|
||||
});
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
public static void remove(Integer appId,String userId,Integer clientType,String imei){
|
||||
UserClientDto dto = new UserClientDto();
|
||||
dto.setAppId(appId);
|
||||
dto.setImei(imei);
|
||||
dto.setClientType(clientType);
|
||||
dto.setUserId(userId);
|
||||
CHANNELS.remove(dto);
|
||||
}
|
||||
|
||||
public static void remove(NioSocketChannel channel){
|
||||
CHANNELS.entrySet().stream().filter(entity -> entity.getValue() == channel)
|
||||
.forEach(entry -> CHANNELS.remove(entry.getKey()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void removeUserSession(NioSocketChannel nioSocketChannel){
|
||||
String userId = (String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get();
|
||||
Integer appId = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.AppId)).get();
|
||||
Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get();
|
||||
String imei = (String) nioSocketChannel
|
||||
.attr(AttributeKey.valueOf(Constants.Imei)).get();
|
||||
|
||||
SessionSocketHolder.remove(appId,userId,clientType,imei);
|
||||
RedissonClient redissonClient = RedisManager.getRedissonClient();
|
||||
RMap<Object, Object> map = redissonClient.getMap(appId +
|
||||
Constants.RedisConstants.UserSessionConstants + userId);
|
||||
map.remove(clientType+":"+imei);
|
||||
|
||||
MessageHeader messageHeader = new MessageHeader();
|
||||
messageHeader.setAppId(appId);
|
||||
messageHeader.setImei(imei);
|
||||
messageHeader.setClientType(clientType);
|
||||
|
||||
UserStatusChangeNotifyPack userStatusChangeNotifyPack = new UserStatusChangeNotifyPack();
|
||||
userStatusChangeNotifyPack.setAppId(appId);
|
||||
userStatusChangeNotifyPack.setUserId(userId);
|
||||
userStatusChangeNotifyPack.setStatus(ImConnectStatusEnum.OFFLINE_STATUS.getCode());
|
||||
|
||||
MqMessageProducer.sendMessage(userStatusChangeNotifyPack,messageHeader, UserEventCommand.USER_ONLINE_STATUS_CHANGE.getCommand());
|
||||
|
||||
nioSocketChannel.close();
|
||||
}
|
||||
|
||||
public static void offlineUserSession(NioSocketChannel nioSocketChannel){
|
||||
String userId = (String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get();
|
||||
Integer appId = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.AppId)).get();
|
||||
Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get();
|
||||
String imei = (String) nioSocketChannel
|
||||
.attr(AttributeKey.valueOf(Constants.Imei)).get();
|
||||
SessionSocketHolder.remove(appId,userId,clientType,imei);
|
||||
RedissonClient redissonClient = RedisManager.getRedissonClient();
|
||||
RMap<String, String> map = redissonClient.getMap(appId +
|
||||
Constants.RedisConstants.UserSessionConstants + userId);
|
||||
String sessionStr = map.get(clientType.toString()+":" + imei);
|
||||
|
||||
if(!StringUtils.isBlank(sessionStr)){
|
||||
UserSession userSession = JSONObject.parseObject(sessionStr, UserSession.class);
|
||||
userSession.setConnectState(ImConnectStatusEnum.OFFLINE_STATUS.getCode());
|
||||
map.put(clientType.toString()+":"+imei,JSONObject.toJSONString(userSession));
|
||||
}
|
||||
|
||||
MessageHeader messageHeader = new MessageHeader();
|
||||
messageHeader.setAppId(appId);
|
||||
messageHeader.setImei(imei);
|
||||
messageHeader.setClientType(clientType);
|
||||
|
||||
UserStatusChangeNotifyPack userStatusChangeNotifyPack = new UserStatusChangeNotifyPack();
|
||||
userStatusChangeNotifyPack.setAppId(appId);
|
||||
userStatusChangeNotifyPack.setUserId(userId);
|
||||
userStatusChangeNotifyPack.setStatus(ImConnectStatusEnum.OFFLINE_STATUS.getCode());
|
||||
|
||||
MqMessageProducer.sendMessage(userStatusChangeNotifyPack,messageHeader, UserEventCommand.USER_ONLINE_STATUS_CHANGE.getCommand());
|
||||
|
||||
nioSocketChannel.close();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
34
hs-im-server/im-tcp/src/main/resources/config.yml
Normal file
34
hs-im-server/im-tcp/src/main/resources/config.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
lim:
|
||||
tcpPort: 29000
|
||||
webSocketPort: 19000
|
||||
bossThreadSize: 1
|
||||
workThreadSize: 8
|
||||
heartBeatTime: 20000 #心跳超时时间 单位毫秒
|
||||
brokerId: 1000
|
||||
loginModel: 3
|
||||
logicUrl: http://127.0.0.1:28000/v1
|
||||
# * 多端同步模式:1 只允许一端在线,手机/电脑/web 踢掉除了本client+imel的设备
|
||||
# * 2 允许手机/电脑的一台设备 + web在线 踢掉除了本client+imel的非web端设备
|
||||
# * 3 允许手机和电脑单设备 + web 同时在线 踢掉非本client+imel的同端设备
|
||||
# * 4 允许所有端多设备登录 不踢任何设备
|
||||
|
||||
redis:
|
||||
mode: single # 单机模式:single 哨兵模式:sentinel 集群模式:cluster
|
||||
database: 8
|
||||
password: dSMIXBQrCBXiHHjk123
|
||||
timeout: 3000 # 超时时间
|
||||
poolMinIdle: 8 #最小空闲数
|
||||
poolConnTimeout: 3000 # 连接超时时间(毫秒)
|
||||
poolSize: 10 # 连接池大小
|
||||
single: #redis单机配置
|
||||
address: 43.139.191.204:6379
|
||||
rabbitmq:
|
||||
host: 192.168.2.180
|
||||
port: 5672
|
||||
virtualHost: /
|
||||
userName: guest
|
||||
password: guest
|
||||
|
||||
zkConfig:
|
||||
zkAddr: 192.168.2.180:2181
|
||||
zkConnectTimeOut: 5000
|
||||
34
hs-im-server/im-tcp/src/main/resources/config2.yml
Normal file
34
hs-im-server/im-tcp/src/main/resources/config2.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
lim:
|
||||
tcpPort: 29002
|
||||
webSocketPort: 19002
|
||||
bossThreadSize: 1
|
||||
workThreadSize: 8
|
||||
heartBeatTime: 20000 #心跳超时时间 单位毫秒
|
||||
brokerId: 1001
|
||||
loginModel: 3
|
||||
logicUrl: http://127.0.0.1:28000/v1
|
||||
# * 多端同步模式:1 只允许一端在线,手机/电脑/web 踢掉除了本client+imel的设备
|
||||
# * 2 允许手机/电脑的一台设备 + web在线 踢掉除了本client+imel的非web端设备
|
||||
# * 3 允许手机和电脑单设备 + web 同时在线 踢掉非本client+imel的同端设备
|
||||
# * 4 允许所有端多设备登录 不踢任何设备
|
||||
|
||||
redis:
|
||||
mode: single # 单机模式:single 哨兵模式:sentinel 集群模式:cluster
|
||||
database: 8
|
||||
password: dSMIXBQrCBXiHHjk123
|
||||
timeout: 3000 # 超时时间
|
||||
poolMinIdle: 8 #最小空闲数
|
||||
poolConnTimeout: 3000 # 连接超时时间(毫秒)
|
||||
poolSize: 10 # 连接池大小
|
||||
single: #redis单机配置
|
||||
address: 43.139.191.204:6379
|
||||
rabbitmq:
|
||||
host: 192.168.2.180
|
||||
port: 5672
|
||||
virtualHost: /
|
||||
userName: guest
|
||||
password: guest
|
||||
|
||||
zkConfig:
|
||||
zkAddr: 192.168.2.180:2181
|
||||
zkConnectTimeOut: 5000
|
||||
51
hs-im-server/im-tcp/src/main/resources/py/heartBeat.py
Normal file
51
hs-im-server/im-tcp/src/main/resources/py/heartBeat.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import socket
|
||||
import json
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
def doPing(scoket):
|
||||
## 基础数据
|
||||
command = 0x270f
|
||||
print(command)
|
||||
version = 1
|
||||
clientType = 4
|
||||
messageType = 0x0
|
||||
appId = 10000
|
||||
userId = 'lld'
|
||||
imei = str(uuid.uuid1())
|
||||
|
||||
## 数据转换为bytes
|
||||
commandByte = command.to_bytes(4,'big')
|
||||
versionByte = version.to_bytes(4,'big')
|
||||
messageTypeByte = messageType.to_bytes(4,'big')
|
||||
clientTypeByte = clientType.to_bytes(4,'big')
|
||||
appIdByte = appId.to_bytes(4,'big')
|
||||
clientTypeByte = clientType.to_bytes(4,'big')
|
||||
imeiBytes = bytes(imei,"utf-8");
|
||||
imeiLength = len(imeiBytes)
|
||||
imeiLengthByte = imeiLength.to_bytes(4,'big')
|
||||
data = {}
|
||||
jsonData = json.dumps(data)
|
||||
body = bytes(jsonData, 'utf-8')
|
||||
body_len = len(body)
|
||||
bodyLenBytes = body_len.to_bytes(4,'big')
|
||||
|
||||
s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body)
|
||||
|
||||
def ping(scoket):
|
||||
while True:
|
||||
time.sleep(10)
|
||||
doPing(scoket)
|
||||
|
||||
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
|
||||
s.connect(("127.0.0.1",9000))
|
||||
|
||||
# 创建一个线程专门接收服务端数据并且打印
|
||||
t1 = threading.Thread(target=ping,args=(s,))
|
||||
t1.start()
|
||||
|
||||
|
||||
|
||||
|
||||
49
hs-im-server/im-tcp/src/main/resources/py/login.py
Normal file
49
hs-im-server/im-tcp/src/main/resources/py/login.py
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
|
||||
import socket
|
||||
import json
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
imei = str(uuid.uuid1())
|
||||
|
||||
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
|
||||
s.connect(("127.0.0.1",9000))
|
||||
|
||||
|
||||
## 基础数据
|
||||
command = 0x2328
|
||||
print(command)
|
||||
version = 1
|
||||
clientType = 4
|
||||
messageType = 0x0
|
||||
appId = 10000
|
||||
userId = 'lld'
|
||||
|
||||
## 数据转换为bytes
|
||||
commandByte = command.to_bytes(4,'big')
|
||||
versionByte = version.to_bytes(4,'big')
|
||||
messageTypeByte = messageType.to_bytes(4,'big')
|
||||
clientTypeByte = clientType.to_bytes(4,'big')
|
||||
appIdByte = appId.to_bytes(4,'big')
|
||||
clientTypeByte = clientType.to_bytes(4,'big')
|
||||
imeiBytes = bytes(imei,"utf-8");
|
||||
imeiLength = len(imeiBytes)
|
||||
imeiLengthByte = imeiLength.to_bytes(4,'big')
|
||||
data = {"userId": userId}
|
||||
jsonData = json.dumps(data)
|
||||
body = bytes(jsonData, 'utf-8')
|
||||
body_len = len(body)
|
||||
bodyLenBytes = body_len.to_bytes(4,'big')
|
||||
|
||||
s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body)
|
||||
# for x in range(100):
|
||||
# s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
71
hs-im-server/im-tcp/src/main/resources/py/loginLongTime.py
Normal file
71
hs-im-server/im-tcp/src/main/resources/py/loginLongTime.py
Normal file
@@ -0,0 +1,71 @@
|
||||
|
||||
|
||||
import socket
|
||||
import json
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
def task(s):
|
||||
print("task开始")
|
||||
while True:
|
||||
# datab = scoket.recv(4)
|
||||
command = struct.unpack('>I', s.recv(4))[0] # 接受command并且解析
|
||||
num = struct.unpack('>I', s.recv(4))[0] # 接受包大小并且解析
|
||||
print(command)
|
||||
if command == 0x232a :
|
||||
print("收到下线通知,退出登录")
|
||||
s.close()
|
||||
# exit;
|
||||
# break
|
||||
|
||||
|
||||
imei = str(uuid.uuid1())
|
||||
|
||||
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
|
||||
s.connect(("127.0.0.1",9000))
|
||||
|
||||
t1 = threading.Thread(target=task,args=(s,))
|
||||
t1.start()
|
||||
|
||||
## 基础数据
|
||||
command = 0x2328
|
||||
|
||||
version = 2
|
||||
clientType = 5
|
||||
print(clientType)
|
||||
messageType = 0x0
|
||||
appId = 10000
|
||||
userId = 'lld'
|
||||
|
||||
## 数据转换为bytes
|
||||
commandByte = command.to_bytes(4,'big')
|
||||
versionByte = version.to_bytes(4,'big')
|
||||
messageTypeByte = messageType.to_bytes(4,'big')
|
||||
clientTypeByte = clientType.to_bytes(4,'big')
|
||||
appIdByte = appId.to_bytes(4,'big')
|
||||
clientTypeByte = clientType.to_bytes(4,'big')
|
||||
imeiBytes = bytes(imei,"utf-8");
|
||||
imeiLength = len(imeiBytes)
|
||||
imeiLengthByte = imeiLength.to_bytes(4,'big')
|
||||
data = {"userId": userId}
|
||||
jsonData = json.dumps(data)
|
||||
body = bytes(jsonData, 'utf-8')
|
||||
body_len = len(body)
|
||||
bodyLenBytes = body_len.to_bytes(4,'big')
|
||||
|
||||
s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body)
|
||||
|
||||
|
||||
# WEB(1,"web"),
|
||||
# IOS(2,"ios"),
|
||||
# ANDROID(3,"android"),
|
||||
# WINDOWS(4,"windows"),
|
||||
# MAC(5,"mac"),
|
||||
|
||||
while(True):
|
||||
i = 1+1
|
||||
|
||||
|
||||
48
hs-im-server/im-tcp/src/main/resources/py/privateSend.py
Normal file
48
hs-im-server/im-tcp/src/main/resources/py/privateSend.py
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
|
||||
import socket
|
||||
import json
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
imei = str(uuid.uuid1())
|
||||
|
||||
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
|
||||
s.connect(("127.0.0.1",9000))
|
||||
|
||||
|
||||
## 基础数据
|
||||
command = 9888
|
||||
version = 1
|
||||
clientType = 4
|
||||
messageType = 0x0
|
||||
appId = 10000
|
||||
name = 'lld'
|
||||
|
||||
## 数据转换为bytes
|
||||
commandByte = command.to_bytes(4,'big')
|
||||
versionByte = version.to_bytes(4,'big')
|
||||
messageTypeByte = messageType.to_bytes(4,'big')
|
||||
clientTypeByte = clientType.to_bytes(4,'big')
|
||||
appIdByte = appId.to_bytes(4,'big')
|
||||
clientTypeByte = clientType.to_bytes(4,'big')
|
||||
imeiBytes = bytes(imei,"utf-8");
|
||||
imeiLength = len(imeiBytes)
|
||||
imeiLengthByte = imeiLength.to_bytes(4,'big')
|
||||
data = {"name": name, "appId": appId, "clientType": clientType, "imei": imei}
|
||||
jsonData = json.dumps(data)
|
||||
body = bytes(jsonData, 'utf-8')
|
||||
body_len = len(body)
|
||||
bodyLenBytes = body_len.to_bytes(4,'big')
|
||||
|
||||
# s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body)
|
||||
for x in range(100):
|
||||
s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
751
hs-im-server/im-tcp/src/main/resources/web.html
Normal file
751
hs-im-server/im-tcp/src/main/resources/web.html
Normal file
@@ -0,0 +1,751 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WebSocket客户端</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
|
||||
|
||||
ByteBuffer = function (arrayBuf, offset) {
|
||||
|
||||
var Type_Byte = 1;
|
||||
var Type_Short = 2;
|
||||
var Type_UShort = 3;
|
||||
var Type_Int32 = 4;
|
||||
var Type_UInt32 = 5;
|
||||
var Type_String = 6;//变长字符串,前两个字节表示长度
|
||||
var Type_VString = 7;//定长字符串
|
||||
var Type_Int64 = 8;
|
||||
var Type_Float = 9;
|
||||
var Type_Double = 10;
|
||||
var Type_ByteArray = 11;
|
||||
|
||||
var _org_buf = arrayBuf ? (arrayBuf.constructor == DataView ? arrayBuf : (arrayBuf.constructor == Uint8Array ? new DataView(arrayBuf.buffer, offset) : new DataView(arrayBuf, offset))) : new DataView(new Uint8Array([]).buffer);
|
||||
var _offset = offset || 0;
|
||||
var _list = [];
|
||||
var _littleEndian = false;
|
||||
|
||||
//指定字节序 为BigEndian
|
||||
this.bigEndian = function () {
|
||||
_littleEndian = false;
|
||||
return this;
|
||||
};
|
||||
|
||||
//指定字节序 为LittleEndian
|
||||
this.littleEndian = function () {
|
||||
_littleEndian = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
if (!ArrayBuffer.prototype.slice) {
|
||||
ArrayBuffer.prototype.slice = function (start, end) {
|
||||
var that = new Uint8Array(this);
|
||||
if (end == undefined) end = that.length;
|
||||
var result = new ArrayBuffer(end - start);
|
||||
var resultArray = new Uint8Array(result);
|
||||
for (var i = 0; i < resultArray.length; i++)
|
||||
resultArray[i] = that[i + start];
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function utf8Write(view, offset, str) {
|
||||
var c = 0;
|
||||
for (var i = 0, l = str.length; i < l; i++) {
|
||||
c = str.charCodeAt(i);
|
||||
if (c < 0x80) {
|
||||
view.setUint8(offset++, c);
|
||||
} else if (c < 0x800) {
|
||||
view.setUint8(offset++, 0xc0 | (c >> 6));
|
||||
view.setUint8(offset++, 0x80 | (c & 0x3f));
|
||||
} else if (c < 0xd800 || c >= 0xe000) {
|
||||
view.setUint8(offset++, 0xe0 | (c >> 12));
|
||||
view.setUint8(offset++, 0x80 | (c >> 6) & 0x3f);
|
||||
view.setUint8(offset++, 0x80 | (c & 0x3f));
|
||||
} else {
|
||||
i++;
|
||||
c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
|
||||
view.setUint8(offset++, 0xf0 | (c >> 18));
|
||||
view.setUint8(offset++, 0x80 | (c >> 12) & 0x3f);
|
||||
view.setUint8(offset++, 0x80 | (c >> 6) & 0x3f);
|
||||
view.setUint8(offset++, 0x80 | (c & 0x3f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function utf8Read(view, offset, length) {
|
||||
var string = '', chr = 0;
|
||||
for (var i = offset, end = offset + length; i < end; i++) {
|
||||
var byte = view.getUint8(i);
|
||||
if ((byte & 0x80) === 0x00) {
|
||||
string += String.fromCharCode(byte);
|
||||
continue;
|
||||
}
|
||||
if ((byte & 0xe0) === 0xc0) {
|
||||
string += String.fromCharCode(
|
||||
((byte & 0x0f) << 6) |
|
||||
(view.getUint8(++i) & 0x3f)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ((byte & 0xf0) === 0xe0) {
|
||||
string += String.fromCharCode(
|
||||
((byte & 0x0f) << 12) |
|
||||
((view.getUint8(++i) & 0x3f) << 6) |
|
||||
((view.getUint8(++i) & 0x3f) << 0)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ((byte & 0xf8) === 0xf0) {
|
||||
chr = ((byte & 0x07) << 18) |
|
||||
((view.getUint8(++i) & 0x3f) << 12) |
|
||||
((view.getUint8(++i) & 0x3f) << 6) |
|
||||
((view.getUint8(++i) & 0x3f) << 0);
|
||||
if (chr >= 0x010000) { // surrogate pair
|
||||
chr -= 0x010000;
|
||||
string += String.fromCharCode((chr >>> 10) + 0xD800, (chr & 0x3FF) + 0xDC00);
|
||||
} else {
|
||||
string += String.fromCharCode(chr);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw new Error('Invalid byte ' + byte.toString(16));
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
function utf8Length(str) {
|
||||
var c = 0, length = 0;
|
||||
for (var i = 0, l = str.length; i < l; i++) {
|
||||
c = str.charCodeAt(i);
|
||||
if (c < 0x80) {
|
||||
length += 1;
|
||||
} else if (c < 0x800) {
|
||||
length += 2;
|
||||
} else if (c < 0xd800 || c >= 0xe000) {
|
||||
length += 3;
|
||||
} else {
|
||||
i++;
|
||||
length += 4;
|
||||
}
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
this.byte = function (val, index) {
|
||||
if (arguments.length == 0) {
|
||||
_list.push(_org_buf.getUint8(_offset, _littleEndian));
|
||||
_offset += 1;
|
||||
} else {
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_Byte, d: val, l: 1});
|
||||
_offset += 1;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
this.short = function (val, index) {
|
||||
if (arguments.length == 0) {
|
||||
_list.push(_org_buf.getInt16(_offset, _littleEndian));
|
||||
_offset += 2;
|
||||
} else {
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_Short, d: val, l: 2});
|
||||
_offset += 2;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
this.ushort = function (val, index) {
|
||||
if (arguments.length == 0) {
|
||||
_list.push(_org_buf.getUint16(_offset, _littleEndian));
|
||||
_offset += 2;
|
||||
} else {
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_UShort, d: val, l: 2});
|
||||
_offset += 2;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
this.int32 = function (val, index) {
|
||||
if (arguments.length == 0) {
|
||||
_list.push(_org_buf.getInt32(_offset, _littleEndian));
|
||||
_offset += 4;
|
||||
} else {
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_Int32, d: val, l: 4});
|
||||
_offset += 4;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
this.uint32 = function (val, index) {
|
||||
if (arguments.length == 0) {
|
||||
_list.push(_org_buf.getUint32(_offset, _littleEndian));
|
||||
_offset += 4;
|
||||
} else {
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_UInt32, d: val, l: 4});
|
||||
_offset += 4;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* 新加的方法,获取bytebuffer的长度
|
||||
*/
|
||||
this.blength = function () {
|
||||
return _offset;
|
||||
};
|
||||
|
||||
/**
|
||||
* 变长字符串 前4个字节表示字符串长度
|
||||
**/
|
||||
this.string = function (val, index) {
|
||||
if (arguments.length == 0) {
|
||||
var len = _org_buf.getInt32(_offset, _littleEndian);
|
||||
_offset += 4;
|
||||
_list.push(utf8Read(_org_buf, _offset, len));
|
||||
_offset += len;
|
||||
} else {
|
||||
var len = 0;
|
||||
if (val) {
|
||||
len = utf8Length(val);
|
||||
}
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_String, d: val, l: len});
|
||||
_offset += len + 4;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* 定长字符串 val为null时,读取定长字符串(需指定长度len)
|
||||
**/
|
||||
this.vstring = function (val, len, index) {
|
||||
if (!len) {
|
||||
throw new Error('vstring must got len argument');
|
||||
return this;
|
||||
}
|
||||
if (val == undefined || val == null) {
|
||||
var vlen = 0;//实际长度
|
||||
for (var i = _offset; i < _offset + len; i++) {
|
||||
if (_org_buf.getUint8(i) > 0) vlen++;
|
||||
}
|
||||
_list.push(utf8Read(_org_buf, _offset, vlen));
|
||||
_offset += len;
|
||||
} else {
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_VString, d: val, l: len});
|
||||
_offset += len;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
this.int64 = function (val, index) {
|
||||
if (arguments.length == 0) {
|
||||
_list.push(_org_buf.getFloat64(_offset, _littleEndian));
|
||||
_offset += 8;
|
||||
} else {
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_Int64, d: val, l: 8});
|
||||
_offset += 8;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
this.float = function (val, index) {
|
||||
if (arguments.length == 0) {
|
||||
_list.push(_org_buf.getFloat32(_offset, _littleEndian));
|
||||
_offset += 4;
|
||||
} else {
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_Float, d: val, l: 4});
|
||||
_offset += 4;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
this.double = function (val, index) {
|
||||
if (arguments.length == 0) {
|
||||
_list.push(_org_buf.getFloat64(_offset, _littleEndian));
|
||||
_offset += 8;
|
||||
} else {
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_Double, d: val, l: 8});
|
||||
_offset += 8;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* 写入或读取一段字节数组
|
||||
**/
|
||||
this.byteArray = function (val, len, index) {
|
||||
if (!len) {
|
||||
throw new Error('byteArray must got len argument');
|
||||
return this;
|
||||
}
|
||||
if (val == undefined || val == null) {
|
||||
var arr = new Uint8Array(_org_buf.buffer.slice(_offset, _offset + len));
|
||||
_list.push(arr);
|
||||
_offset += len;
|
||||
} else {
|
||||
_list.splice(index != undefined ? index : _list.length, 0, {t: Type_ByteArray, d: val, l: len});
|
||||
_offset += len;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解包成数据数组
|
||||
**/
|
||||
this.unpack = function () {
|
||||
return _list;
|
||||
};
|
||||
|
||||
/**
|
||||
* 打包成二进制,在前面加上4个字节表示包长
|
||||
**/
|
||||
this.packWithHead = function () {
|
||||
return this.pack(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* 打包成二进制
|
||||
* @param ifHead 是否在前面加上4个字节表示包长
|
||||
**/
|
||||
this.pack = function (ifHead) {
|
||||
_org_buf = new DataView(new ArrayBuffer((ifHead) ? _offset + 4 : _offset));
|
||||
var offset = 0;
|
||||
if (ifHead) {
|
||||
_org_buf.setUint32(offset, _offset, _littleEndian);
|
||||
offset += 4;
|
||||
}
|
||||
for (var i = 0; i < _list.length; i++) {
|
||||
switch (_list[i].t) {
|
||||
case Type_Byte:
|
||||
_org_buf.setInt8(offset, _list[i].d);
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
case Type_Short:
|
||||
_org_buf.setInt16(offset, _list[i].d, _littleEndian);
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
case Type_UShort:
|
||||
_org_buf.setUint16(offset, _list[i].d, _littleEndian);
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
case Type_Int32:
|
||||
_org_buf.setInt32(offset, _list[i].d, _littleEndian);
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
case Type_UInt32:
|
||||
_org_buf.setUint32(offset, _list[i].d, _littleEndian);
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
case Type_String:
|
||||
//前4个字节表示字符串长度
|
||||
_org_buf.setUint32(offset, _list[i].l, _littleEndian);
|
||||
offset += 4;
|
||||
utf8Write(_org_buf, offset, _list[i].d);
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
case Type_VString:
|
||||
utf8Write(_org_buf, offset, _list[i].d);
|
||||
var vlen = utf8Length(_list[i].d);//字符串实际长度
|
||||
//补齐\0
|
||||
for (var j = offset + vlen; j < offset + _list[i].l; j++) {
|
||||
_org_buf.setUint8(j, 0);
|
||||
}
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
case Type_Int64:
|
||||
_org_buf.setFloat64(offset, _list[i].d, _littleEndian);
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
case Type_Float:
|
||||
_org_buf.setFloat32(offset, _list[i].d, _littleEndian);
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
case Type_Double:
|
||||
_org_buf.setFloat64(offset, _list[i].d, _littleEndian);
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
case Type_ByteArray:
|
||||
var indx = 0;
|
||||
for (var j = offset; j < offset + _list[i].l; j++) {
|
||||
if (indx < _list[i].d.length) {
|
||||
_org_buf.setUint8(j, _list[i].d[indx]);
|
||||
} else {//不够的话,后面补齐0x00
|
||||
_org_buf.setUint8(j, 0);
|
||||
}
|
||||
indx++
|
||||
}
|
||||
offset += _list[i].l;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return _org_buf.buffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* 未读数据长度
|
||||
**/
|
||||
this.getAvailable = function () {
|
||||
if (!_org_buf) return _offset;
|
||||
return _org_buf.buffer.byteLength - _offset;
|
||||
};
|
||||
}
|
||||
|
||||
function uuid() {
|
||||
var s = [];
|
||||
var hexDigits = "0123456789abcdef";
|
||||
for (var i = 0; i < 36; i++) {
|
||||
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
|
||||
}
|
||||
s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
|
||||
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
|
||||
s[8] = s[13] = s[18] = s[23] = "-";
|
||||
|
||||
var uuid = s.join("");
|
||||
return uuid;
|
||||
}
|
||||
|
||||
var socket;
|
||||
|
||||
//如果浏览器支持WebSocket
|
||||
if (window.WebSocket) {
|
||||
//参数就是与服务器连接的地址
|
||||
socket = new WebSocket("ws://localhost:19000/ws");
|
||||
|
||||
//客户端收到服务器消息的时候就会执行这个回调方法
|
||||
socket.onmessage = function (event) {
|
||||
var ta = document.getElementById("responseText");
|
||||
var userId = document.getElementById("userId").value;
|
||||
|
||||
var bytebuf = new ByteBuffer(event.data);
|
||||
let byteBuffer = bytebuf.int32().int32().unpack();
|
||||
|
||||
let command = byteBuffer[0];
|
||||
let bodyLen = byteBuffer[1];
|
||||
let unpack = bytebuf.vstring(null, bodyLen).unpack();
|
||||
let msgBody = unpack[2];
|
||||
var version = 1;
|
||||
|
||||
var clientType = document.getElementById("clientType").value;
|
||||
clientType = parseInt(clientType);
|
||||
var messageType = 0x0;
|
||||
var appId = document.getElementById("appId").value;
|
||||
appId = parseInt(appId)
|
||||
|
||||
var imei = document.getElementById("imei").value;
|
||||
var imeiLen = getLen(imei);
|
||||
|
||||
console.log("收到服务端发来的消息: " + msgBody);
|
||||
|
||||
if(command == 1103){
|
||||
var d = JSON.parse(msgBody)
|
||||
var data = d.data;
|
||||
let userId = document.getElementById('userId').value;
|
||||
if(data.fromId == userId){
|
||||
ta.value = ta.value + "\n" + "自己:" + data.messageBody;
|
||||
}else if(data.fromId != userId){
|
||||
ta.value = ta.value + "\n" + data.fromId + ":" + data.messageBody;
|
||||
}
|
||||
|
||||
// data.fromId = data.toId;
|
||||
// data.conversationId = "0_" + data.toId + "_" + data.fromId;
|
||||
// data.conversationType = 0;
|
||||
|
||||
if(userId != data.fromId){
|
||||
var rAck = {
|
||||
"fromId":userId,
|
||||
"toId":data.fromId,
|
||||
"messageKey":data.messageKey,
|
||||
"messageId":data.messageId,
|
||||
"messageSequence":data.messageSequence,
|
||||
}
|
||||
|
||||
let messageReciver = new ByteBuffer();
|
||||
var jsonData = JSON.stringify(rAck);
|
||||
let bodyLen = jsonData.length;
|
||||
messageReciver.int32(1107)
|
||||
.int32(version).int32(clientType)
|
||||
.int32(messageType).int32(appId)
|
||||
.int32(imeiLen).int32(bodyLen).vstring(imei,imeiLen)
|
||||
.vstring(jsonData, bodyLen);
|
||||
socket.send(messageReciver.pack());
|
||||
}
|
||||
|
||||
if(clientType == 1 ){
|
||||
let messageReader = new ByteBuffer();
|
||||
var toId = data.fromId;
|
||||
if(data.fromId == userId){
|
||||
toId = data.toId;
|
||||
}
|
||||
var readedData = {
|
||||
"fromId":userId,
|
||||
"toId":toId,
|
||||
"conversationType":0,
|
||||
"messageSequence":data.messageSequence,
|
||||
}
|
||||
var readData = JSON.stringify(readedData);
|
||||
let readBodyLen = readData.length;
|
||||
messageReader.int32(1106)
|
||||
.int32(version).int32(clientType)
|
||||
.int32(messageType).int32(appId)
|
||||
.int32(imeiLen).int32(readBodyLen).vstring(imei,imeiLen)
|
||||
.vstring(readData, readBodyLen);
|
||||
socket.send(messageReader.pack());
|
||||
}
|
||||
|
||||
}else if(command == 2104){
|
||||
var d = JSON.parse(msgBody)
|
||||
var data = d.data;
|
||||
let userId = document.getElementById('userId').value;
|
||||
|
||||
if(clientType == 1){
|
||||
let messageReader = new ByteBuffer();
|
||||
var toId = data.fromId;
|
||||
if(data.fromId == userId){
|
||||
toId = data.toId;
|
||||
}
|
||||
var readedData = {
|
||||
"fromId":userId,
|
||||
"toId":data.fromId,
|
||||
"groupId":data.groupId,
|
||||
"conversationType":1,
|
||||
"messageSequence":data.messageSequence,
|
||||
}
|
||||
var readData = JSON.stringify(readedData);
|
||||
let readBodyLen = readData.length;
|
||||
messageReader.int32(2106)
|
||||
.int32(version).int32(clientType)
|
||||
.int32(messageType).int32(appId)
|
||||
.int32(imeiLen).int32(readBodyLen).vstring(imei,imeiLen)
|
||||
.vstring(readData, readBodyLen);
|
||||
socket.send(messageReader.pack());
|
||||
}
|
||||
}
|
||||
|
||||
else if(command == 9999){
|
||||
var msg = eval("(" + msgBody + ")");
|
||||
console.log(msg)
|
||||
console.log(userId)
|
||||
|
||||
if (msg["userId"] == "system") {
|
||||
ta.value = ta.value + "\n 系统:" + msg.data;
|
||||
} else if (msg["userId"] == userId) {
|
||||
let msgInfo = msg.data;
|
||||
msgInfo = eval("(" + msgInfo + ")");
|
||||
ta.value = ta.value + "\n" + "自己:" + msgInfo.msgBody;
|
||||
} else {
|
||||
let msgInfo = msg.data;
|
||||
msgInfo = eval("(" + msgInfo + ")");
|
||||
ta.value = ta.value + "\n" + msg.toId + ":" + msgInfo.msgBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//连接建立的回调函数
|
||||
socket.onopen = function (event) {
|
||||
socket.binaryType = "arraybuffer";
|
||||
var ta = document.getElementById("responseText");
|
||||
ta.value = "连接开启";
|
||||
}
|
||||
|
||||
//连接断掉的回调函数
|
||||
socket.onclose = function (event) {
|
||||
var ta = document.getElementById("responseText");
|
||||
ta.value = ta.value + "\n" + "连接关闭";
|
||||
}
|
||||
socket.onerror = function (event) {
|
||||
var ta = document.getElementById("responseText");
|
||||
ta.value = ta.value + "\n" + "连接失败";
|
||||
}
|
||||
} else {
|
||||
alert("浏览器不支持WebSocket!");
|
||||
}
|
||||
|
||||
//登录
|
||||
function login(userId, toUser,clientType,imei,appId) {
|
||||
if (!window.WebSocket) {
|
||||
return;
|
||||
}
|
||||
console.log(userId + " " + toUser);
|
||||
//当websocket状态打开
|
||||
if (socket.readyState == WebSocket.OPEN) {
|
||||
|
||||
var command = 9000;
|
||||
var version = 1;
|
||||
if(clientType == null){
|
||||
clientType = 1;
|
||||
}
|
||||
clientType = parseInt(clientType);
|
||||
|
||||
var messageType = 0x0;
|
||||
|
||||
if(imei == null){
|
||||
imei = "web";
|
||||
}
|
||||
|
||||
if(appId == null){
|
||||
appId = '10000'
|
||||
}
|
||||
appId = parseInt(appId);
|
||||
|
||||
var userId = userId;
|
||||
|
||||
var data = {"userId": userId, "appId": 10000, "clientType": clientType, "imei": imei,"customStatus":null,"customClientName":""};
|
||||
var jsonData = JSON.stringify(data);
|
||||
console.log(jsonData);
|
||||
|
||||
var bodyLen = jsonData.length;
|
||||
var imeiLen = getLen(imei);
|
||||
let loginMsg = new ByteBuffer();
|
||||
loginMsg.int32(command).int32(version)
|
||||
.int32(clientType).int32(messageType)
|
||||
.int32(appId).int32(imeiLen)
|
||||
.int32(bodyLen).vstring(imei,imeiLen)
|
||||
.vstring(jsonData, bodyLen);
|
||||
socket.send(loginMsg.pack());
|
||||
} else {
|
||||
alert("连接没有开启");
|
||||
}
|
||||
}
|
||||
|
||||
// * 私有协议规则,
|
||||
// * 4位表示Command表示消息的开始,
|
||||
// * 4位表示version
|
||||
// * 4位表示clientType
|
||||
// * 4位表示messageType
|
||||
// * 4位表示appId(待定)
|
||||
// * 4位表示数据长度
|
||||
// * 后续将解码方式加到数据头根据不同的解码方式解码,如pb,json,现在用json字符串
|
||||
//发消息
|
||||
function sendMsg(userId, toId, command,msg,clientType,imei,appId) {
|
||||
if (!window.WebSocket) {
|
||||
return;
|
||||
}
|
||||
console.log(msg)
|
||||
//当websocket状态打开
|
||||
if (socket.readyState == WebSocket.OPEN) {
|
||||
// debugger;
|
||||
//var command = 1103;
|
||||
|
||||
|
||||
// var command = 9000;
|
||||
var version = 1;
|
||||
if(clientType == null ){
|
||||
clientType = 1;
|
||||
}
|
||||
|
||||
clientType = parseInt(clientType);
|
||||
|
||||
var messageType = 0x0;
|
||||
|
||||
var userId = userId;
|
||||
|
||||
if(command == null || command == ''){
|
||||
command = 1103;
|
||||
}
|
||||
|
||||
if(imei == null){
|
||||
imei = "web"
|
||||
}
|
||||
if(appId == null){
|
||||
appId = '10000'
|
||||
}
|
||||
appId = parseInt(appId);
|
||||
var data = {
|
||||
"userId": userId,
|
||||
"groupId": toId,
|
||||
"appId": appId,
|
||||
"clientType": 1,
|
||||
"imei": imei,
|
||||
"command": command
|
||||
}
|
||||
|
||||
var messageId = uuid();
|
||||
if(msg === 'lld'){
|
||||
messageId = msg;
|
||||
}
|
||||
var messageData = {};
|
||||
if(command == 1103){
|
||||
messageData = {
|
||||
"messageId": messageId,
|
||||
"fromId": userId,
|
||||
"toId": toId,
|
||||
"appId": appId,
|
||||
"clientType": clientType,
|
||||
"imei": imei,
|
||||
"messageBody": msg
|
||||
}
|
||||
}else if (command == 2104){
|
||||
messageData = {
|
||||
"messageId": messageId,
|
||||
"fromId": userId,
|
||||
"groupId": toId,
|
||||
"appId": appId,
|
||||
"clientType": clientType,
|
||||
"imei": imei,
|
||||
"messageBody": msg
|
||||
}
|
||||
}else{
|
||||
messageData = JSON.parse(msg)
|
||||
}
|
||||
// data.data = messageData;
|
||||
|
||||
var jsonData = JSON.stringify(messageData);
|
||||
console.log(jsonData)
|
||||
var bodyLen = getLen(jsonData);
|
||||
var imeiLen = getLen(imei);
|
||||
let sendMsg = new ByteBuffer();
|
||||
sendMsg.int32(command).int32(version)
|
||||
.int32(clientType).int32(messageType)
|
||||
.int32(appId).int32(imeiLen)
|
||||
.int32(bodyLen).vstring(imei,imeiLen)
|
||||
.vstring(jsonData, bodyLen);
|
||||
console.log(sendMsg.pack());
|
||||
socket.send(sendMsg.pack());
|
||||
} else {
|
||||
alert("连接没有开启");
|
||||
}
|
||||
}
|
||||
|
||||
function getLen(str) {
|
||||
var len = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
var c = str.charCodeAt(i);
|
||||
//单字节加1
|
||||
if ((c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f)) {
|
||||
len++;
|
||||
} else {
|
||||
len += 3;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<form onsubmit="return false">
|
||||
|
||||
<textarea placeholder="输入登录id" id="userId" name="userId" style="width: 100px;height: 20px"></textarea>
|
||||
<textarea placeholder="你要发消息给谁" name="toUser" style="width: 100px;height: 20px"></textarea>
|
||||
<textarea placeholder="appId" id="appId" name="appId" style="width: 100px;height: 20px"></textarea>
|
||||
<textarea placeholder="clientType" id="clientType" name="clientType" style="width: 100px;height: 20px"></textarea>
|
||||
<textarea placeholder="imei" id="imei" name="imei" style="width: 100px;height: 20px"></textarea>
|
||||
<input type="button" value="login" onclick="login(this.form.userId.value,this.form.toUser.value,this.form.clientType.value
|
||||
,this.form.imei.value,this.form.appId.value);">
|
||||
|
||||
<textarea placeholder="输入command" name="command" style="width: 200px;height: 20px"></textarea>
|
||||
|
||||
<textarea placeholder="输入要发送的内容" name="message" style="width: 200px;height: 20px"></textarea>
|
||||
|
||||
<input type="button" value="发送数据"
|
||||
onclick="sendMsg(this.form.userId.value,this.form.toUser.value,this.form.command.value,this.form.message.value
|
||||
,this.form.clientType.value,this.form.imei.value,this.form.appId.value);">
|
||||
|
||||
<h3>服务器输出:</h3>
|
||||
|
||||
<textarea id="responseText" style="width: 400px;height: 300px;"></textarea>
|
||||
|
||||
<input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空数据">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user