移动app

This commit is contained in:
2023-09-24 17:55:19 +08:00
parent 736c5376e0
commit 59f7e39791
735 changed files with 80523 additions and 57 deletions

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>im-system</artifactId>
<groupId>com.lld</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tcp</artifactId>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.3.0</mybatis-plus.version>
<hutool.version>5.0.6</hutool.version>
</properties>
<dependencies>
<!-- commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>
<dependency>
<groupId>com.lld</groupId>
<artifactId>common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<!-- netty -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<!-- yaml解析 -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
<!-- rabbitmq -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
</dependency>
<!-- feign调用依赖 -->
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-jackson</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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会帮你处理一些繁重的复杂的事
* 会帮你处理握手动作: handshakingclose, 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());
}
}

View File

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

View File

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

View 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

View 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

View 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()

View 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)

View 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

View 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)

View 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位表示数据长度
// * 后续将解码方式加到数据头根据不同的解码方式解码如pbjson现在用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>