客户端给服务端发送信息,客户端形式有多重,比如安卓、IOS、H5页面都可以作为客户端与netty服务端连接并发送信息,当然里面的协议可以有多种,比如HTTP协议、WebSocket协议、自定义协议等。今天我们分享http协议,netty为核心的web客户端与服务端建立连接聊天功能。
1、jar包引入和服务端一致,配置稍有区别:
spring.application.name=tim-client
# web port
server.port=8003
# 是否打开swagger
#swagger.enable=true
logging.level.root=error
# 消息记录存放路径
tim.msg.logger.path=/opt/logs/tim/
# gateway url suggested that this is Nginx address
tim.gateway.url=http://localhost:8090/
# 客户端唯一ID
#tim.user.id=1633958598301
#tim.user.userName=zhangsan
#tim.user.id=1633958841165
#tim.user.userName=lisi
tim.user.id=1633958926300
tim.user.userName=wangwu
# 回调线程队列大小
tim.callback.thread.queue.size=2
# 回调线程池大小
tim.callback.thread.pool.size=2
# 检测多少秒没有收到服务端端心跳后重新登录获取连接
tim.heartbeat.time=60
# 客户端连接失败重连次数
tim.reconnect.count=3
2、boot启动类:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TIMClientApplication implements CommandLineRunner {
private final static Logger LOGGER = LoggerFactory.getLogger(TIMClientApplication.class);
@Autowired
private ClientInfo clientInfo;
public static void main(String[] args) {
SpringApplication.run(TIMClientApplication.class, args);
LOGGER.info("启动 Client 服务成功");
}
@Override
public void run(String... args) throws Exception {
Scan scan = new Scan();
Thread thread = new Thread(scan);
thread.setName("scan-thread");
thread.start();
clientInfo.saveStartDate();
}
}
一会发送消息的Scan类:
import com.vdurmont.emoji.EmojiParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Scanner;
/**
* @since JDK 1.8
*/
public class Scan implements Runnable {
private final static Logger LOGGER = LoggerFactory.getLogger(Scan.class);
/**
* 系统参数
*/
private AppConfiguration configuration;
private MsgHandle msgHandle;
private MsgLogger msgLogger;
private EchoService echoService;
public Scan() {
this.configuration = SpringBeanFactory.getBean(AppConfiguration.class);
this.msgHandle = SpringBeanFactory.getBean(MsgHandle.class);
this.msgLogger = SpringBeanFactory.getBean(MsgLogger.class);
this.echoService = SpringBeanFactory.getBean(EchoService.class);
}
@Override
public void run() {
Scanner sc = new Scanner(System.in);
while (true) {
String msg = sc.nextLine();
//检查消息
if (msgHandle.checkMsg(msg)) {
continue;
}
//系统内置命令
if (msgHandle.innerCommand(msg)) {
continue;
}
//真正的发送消息
msgHandle.sendMsg(msg);
//写入聊天记录
msgLogger.log(msg);
echoService.echo(EmojiParser.parseToUnicode(msg));
}
}
}
一会详细分析此类
3、netty 客户端启动类:
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @since JDK 1.8
*/
@Component
public class TIMClient {
private final static Logger LOGGER = LoggerFactory.getLogger(TIMClient.class);
private EventLoopGroup group = new NioEventLoopGroup(1, new DefaultThreadFactory("tim-work"));
@Value("${tim.user.id}")
private long userId;
@Value("${tim.user.userName}")
private String userName;
private SocketChannel channel;
@Autowired
private EchoService echoService;
@Autowired
private RouteRequest routeRequest;
@Autowired
private AppConfiguration configuration;
@Autowired
private MsgHandle msgHandle;
@Autowired
private ClientInfo clientInfo;
@Autowired
private ReConnectManager reConnectManager;
/**
* 重试次数
*/
private int errorCount;
@PostConstruct
public void start() throws Exception {
//登录 + 获取可以使用的服务器 ip+port
TIMServerResVO.ServerInfo timServer = userLogin();
//启动客户端
startClient(timServer);
//向服务端注册
loginTIMServer();
}
/**
* 启动客户端
*
* @param timServer
* @throws Exception
*/
private void startClient(TIMServerResVO.ServerInfo timServer) {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new TIMClientHandleInitializer());
ChannelFuture future = null;
try {
future = bootstrap.connect(timServer.getIp(), timServer.getTimServerPort()).sync();
} catch (Exception e) {
errorCount++;
if (errorCount >= configuration.getErrorCount()) {
LOGGER.error("连接失败次数达到上限[{}]次", errorCount);
msgHandle.shutdown();
}
LOGGER.error("Connect fail!", e);
}
if (future.isSuccess()) {
echoService.echo("Start tim client success!");
LOGGER.info("启动 tim client 成功");
}
channel = (SocketChannel) future.channel();
}
/**
* 登录+路由服务器
*
* @return 路由服务器信息
* @throws Exception
*/
private TIMServerResVO.ServerInfo userLogin() {
LoginReqVO loginReqVO = new LoginReqVO(userId, userName);
TIMServerResVO.ServerInfo timServer = null;
try {
timServer = routeRequest.getTIMServer(loginReqVO);
//保存系统信息
clientInfo.saveServiceInfo(timServer.getIp() + ":" + timServer.getTimServerPort())
.saveUserInfo(userId, userName);
LOGGER.info("timServer=[{}]", timServer.toString());
} catch (Exception e) {
errorCount++;
if (errorCount >= configuration.getErrorCount()) {
echoService.echo("The maximum number of reconnections has been reached[{}]times, close tim client!", errorCount);
msgHandle.shutdown();
}
LOGGER.error("login fail", e);
}
return timServer;
}
/**
* 向服务器注册
*/
private void loginTIMServer() {
TIMReqMsg login = new TIMReqMsg(userId, userName, Constants.CommandType.LOGIN);
ChannelFuture future = channel.writeAndFlush(login);
future.addListener((ChannelFutureListener) channelFuture ->
echoService.echo("Registry tim server success!")
);
}
/**
* 发送消息字符串
*
* @param msg
*/
public void sendStringMsg(String msg) {
ByteBuf message = Unpooled.buffer(msg.getBytes().length);
message.writeBytes(msg.getBytes());
ChannelFuture future = channel.writeAndFlush(message);
future.addListener((ChannelFutureListener) channelFuture ->
LOGGER.info("客户端手动发消息成功={}", msg));
}
/**
* 发送 Google Protocol 编解码字符串
*
* @param googleProtocolVO
*/
public void sendGoogleProtocolMsg(GoogleProtocolVO googleProtocolVO) {
TIMReqMsg protocol = new TIMReqMsg(googleProtocolVO.getRequestId(), googleProtocolVO.getMsg(), Constants.CommandType.MSG);
ChannelFuture future = channel.writeAndFlush(protocol);
future.addListener((ChannelFutureListener) channelFuture ->
LOGGER.info("客户端手动发送 Google Protocol 成功={}", googleProtocolVO.toString()));
}
/**
* 1. clear route information.
* 2. reconnect.
* 3. shutdown reconnect job.
* 4. reset reconnect state.
*
* @throws Exception
*/
public void reconnect() throws Exception {
if (channel != null && channel.isActive()) {
return;
}
//首先清除路由信息,下线
routeRequest.offLine();
echoService.echo("tim server shutdown, reconnecting....");
start();
echoService.echo("Great! reConnect success!!!");
reConnectManager.reConnectSuccess();
ContextHolder.clear();
}
/**
* 关闭
*
* @throws InterruptedException
*/
public void close() throws InterruptedException {
if (channel != null) {
channel.close();
}
}
}
TIMClientHandleInitializer分析:
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.timeout.IdleStateHandler;
/**
* @since JDK 1.8
*/
public class TIMClientHandleInitializer extends ChannelInitializer<Channel> {
private final TIMClientHandle TIMClientHandle = new TIMClientHandle();
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
// 15秒客户端没给服务端主动发送消息就触发写空闲,执行TIMClientHandle的userEventTriggered方法给服务端发送一次心跳
.addLast(new IdleStateHandler(0, 15, 0))
.addLast(new ObjEncoder(TIMReqMsg.class))
.addLast(new ObjDecoder(TIMReqMsg.class))
.addLast(TIMClientHandle)
;
}
}
4、TIMClientHandle 核心:
import com.vdurmont.emoji.EmojiParser;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @since JDK 1.8
*/
public class TIMClientHandle extends SimpleChannelInboundHandler<TIMReqMsg> {
private final static Logger LOGGER = LoggerFactory.getLogger(TIMClientHandle.class);
private MsgHandleCaller caller;
private ThreadPoolExecutor threadPoolExecutor;
private ScheduledExecutorService scheduledExecutorService;
private ReConnectManager reConnectManager;
private ShutDownMsg shutDownMsg;
private EchoService echoService;
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
if (idleStateEvent.state() == IdleState.WRITER_IDLE) {
TIMReqMsg heartBeat = SpringBeanFactory.getBean("heartBeat", TIMReqMsg.class);
//System.out.println("客户端给服务端发送心跳");
ctx.writeAndFlush(heartBeat).addListeners((ChannelFutureListener) future -> {
if (!future.isSuccess()) {
LOGGER.error("IO error,close Channel");
future.channel().close();
}
});
}
}
super.userEventTriggered(ctx, evt);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//客户端和服务端建立连接时调用
LOGGER.info("tim server connect success!");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (shutDownMsg == null) {
shutDownMsg = SpringBeanFactory.getBean(ShutDownMsg.class);
}
//用户主动退出,不执行重连逻辑
if (shutDownMsg.checkStatus()) {
return;
}
if (scheduledExecutorService == null) {
scheduledExecutorService = SpringBeanFactory.getBean("scheduledTask", ScheduledExecutorService.class);
reConnectManager = SpringBeanFactory.getBean(ReConnectManager.class);
}
LOGGER.info("客户端断开了,重新连接!");
reConnectManager.reConnect(ctx);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TIMReqMsg msg) throws Exception {
if (echoService == null) {
echoService = SpringBeanFactory.getBean(EchoServiceImpl.class);
}
//心跳更新时间
if (msg.getType() == Constants.CommandType.PING) {
//LOGGER.info("收到服务端心跳!!!");
NettyAttrUtil.updateReaderTime(ctx.channel(), System.currentTimeMillis());
}
if (msg.getType() != Constants.CommandType.PING) {
//回调消息
callBackMsg(msg.getReqMsg());
//将消息中的 emoji 表情格式化为 Unicode 编码以便在终端可以显示
String response = EmojiParser.parseToUnicode(msg.getReqMsg());
echoService.echo(response);
}
}
/**
* 回调消息
*
* @param msg
*/
private void callBackMsg(String msg) {
threadPoolExecutor = SpringBeanFactory.getBean("callBackThreadPool", ThreadPoolExecutor.class);
threadPoolExecutor.execute(() -> {
caller = SpringBeanFactory.getBean(MsgHandleCaller.class);
caller.getMsgHandleListener().handle(msg);
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//异常时断开连接
cause.printStackTrace();
ctx.close();
}
}
5、工厂获取对象
package com.tuling.tim.client.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public final class SpringBeanFactory implements ApplicationContextAware{
private static ApplicationContext context;
public static <T> T getBean(Class<T> c){
return context.getBean(c);
}
public static <T> T getBean(String name,Class<T> clazz){
return context.getBean(name,clazz);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
}
6、可以通过控制层发送消息:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @since JDK 1.8
*/
@Controller
@RequestMapping("/")
public class IndexController {
@Autowired
private TIMClient heartbeatClient;
@Autowired
private RouteRequest routeRequest;
/**
* 向服务端发消息 字符串
*
* @param stringReqVO
* @return
*/
@RequestMapping(value = "sendStringMsg", method = RequestMethod.POST)
@ResponseBody
public BaseResponse<NULLBody> sendStringMsg(@RequestBody StringReqVO stringReqVO) {
BaseResponse<NULLBody> res = new BaseResponse();
for (int i = 0; i < 100; i++) {
heartbeatClient.sendStringMsg(stringReqVO.getMsg());
}
SendMsgResVO sendMsgResVO = new SendMsgResVO();
sendMsgResVO.setMsg("OK");
res.setCode(StatusEnum.SUCCESS.getCode());
res.setMessage(StatusEnum.SUCCESS.getMessage());
return res;
}
/**
* 向服务端发消息 Google ProtoBuf
*
* @param googleProtocolVO
* @return
*/
@RequestMapping(value = "sendProtoBufMsg", method = RequestMethod.POST)
@ResponseBody
public BaseResponse<NULLBody> sendProtoBufMsg(@RequestBody GoogleProtocolVO googleProtocolVO) {
BaseResponse<NULLBody> res = new BaseResponse();
for (int i = 0; i < 100; i++) {
heartbeatClient.sendGoogleProtocolMsg(googleProtocolVO);
}
SendMsgResVO sendMsgResVO = new SendMsgResVO();
sendMsgResVO.setMsg("OK");
res.setCode(StatusEnum.SUCCESS.getCode());
res.setMessage(StatusEnum.SUCCESS.getMessage());
return res;
}
/**
* 群发消息
*
* @param sendMsgReqVO
* @return
*/
@RequestMapping(value = "sendGroupMsg", method = RequestMethod.POST)
@ResponseBody
public BaseResponse sendGroupMsg(@RequestBody SendMsgReqVO sendMsgReqVO) throws Exception {
BaseResponse<NULLBody> res = new BaseResponse();
GroupReqVO groupReqVO = new GroupReqVO(sendMsgReqVO.getUserId(), sendMsgReqVO.getMsg());
routeRequest.sendGroupMsg(groupReqVO);
res.setCode(StatusEnum.SUCCESS.getCode());
res.setMessage(StatusEnum.SUCCESS.getMessage());
return res;
}
}
也可以通过 Scan 类来发消息。
通过netty客户端与服务端聊天分享到此,下篇我们分析安卓或IOS端通过websocket协议和服务交互聊天,敬请期待!