原来打算做个多人斗地主练习程序,但那须要织入过多的业务逻辑,因此一方面会带来不必要的理解难度,让案例更为繁芜化,另一方面代码量也会偏多,以是终极依旧选择实现基本的IM谈天程序,既大略,又能加深对Netty的理解。
2、配套源码
本文配套源码的开源托管地址是:
关于 Netty 是什么,这里大略先容下:

Netty 是一个 Java 开源框架。Netty 供应异步的、事宜驱动的网络运用程序框架和工具,用以快速开拓高性能、高可靠性的网络做事器和客户端程序。
也便是说,Netty 是一个基于 NIO 的客户、做事器端编程框架,利用Netty 可以确保你快速和大略的开拓出一个网络运用,例如实现了某种协议的客户,做事端运用。
Netty 相称简化和流线化了网络运用的编程开拓过程,例如,TCP 和 UDP 的 Socket 做事开拓。
有关Netty的入门文章:
1)新手入门:目前为止最透彻的的Netty高性能事理和框架架构解析2)写给初学者:Java高性能NIO框架Netty的学习方法和进阶策略3)史上最普通Netty框架入门长文:基本先容、环境搭建、动手实战如果你连Java NIO都不知道,下面的文章建议优先读:
1)少啰嗦!一分钟带你读懂Java的NIO和经典IO的差异2)史上最强Java NIO入门:担心从入门到放弃的,请读这篇!
3)Java的BIO和NIO很难懂?用代码实践给你看,再不懂我转行!
Netty源码和API 在线查阅地址:
1)Netty-4.1.x 完全源码(在线阅读版)2)Netty-4.1.x API文档(在线版)4、基于Netty设计通信协议协议,这玩意儿相信大家肯定不陌生了,大略回顾一下协议的观点:网络协议是指一种通信双方都必须遵守的约定,两个不同的端,按照一定的格式对数据进行“编码”,同时按照相同的规则进行“解码”,从而实现两者之间的数据传输与通信。
当自己想要打造一款IM通信程序时,对付的封装、拆分也同样须要设计一个协议,通信的两端都必须遵守该协议事情,这也是实现通信程序的条件。
但为什么须要通信协议呢?
由于TCP/IP中是基于流的办法传输,与之间没有边界,而协议的目的则在于约定的样式、边界等。
5、Redis通信的RESP协议参考学习不知大家是否还记得之前我聊到的RESP客户端协议,这是Redis供应的一种客户端通信协议。如果想要操作Redis,就必须遵守该协议的格式发送数据。
这个协议特殊大略,如下:
1)首先哀求所有命令,都以开头,后面随着详细的子命令数量,接着用换行符分割;2)接着须要先用$符号声明每个子命令的长度,然后再用换行符分割;3)末了再拼接上详细的子命令,同样用换行符分割。这样描述有些令人难懂,那就直接看个案例,例如一条大略set命令。
如下:
客户端命令:
setname ZhuZi
转变为RESP指令:
3
$3
set
$4
name
$5
ZhuZi
按照Redis的规定,但凡知足RESP协议的客户端,都可以直接连接并操作Redis做事端,这也就意味着咱们可以直接通过Netty来手写一个Redis客户端。
代码如下:
// 基于Netty、RESP协议实现的Redis客户端
publicclassRedisClient {
// 换行符的ASCII码
staticfinalbyte[] LINE = {13, 10};
publicstaticvoidmain(String[] args) {
EventLoopGroup worker = newNioEventLoopGroup();
Bootstrap client = newBootstrap();
try{
client.group(worker);
client.channel(NioSocketChannel.class);
client.handler(newChannelInitializer<SocketChannel>() {
@Override
protectedvoidinitChannel(SocketChannel socketChannel)
throwsException {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(newChannelInboundHandlerAdapter(){
// 通道建立成功后调用:向Redis发送一条set命令
@Override
publicvoidchannelActive(ChannelHandlerContext ctx)
throwsException {
String command = "set name ZhuZi";
ByteBuf buffer = respCommand(command);
ctx.channel().writeAndFlush(buffer);
}
// Redis相应数据时触发:打印Redis的相应结果
@Override
publicvoidchannelRead(ChannelHandlerContext ctx,
Object msg) throwsException {
// 接管Redis做事端实行指令后的结果
ByteBuf buffer = (ByteBuf) msg;
System.out.println(buffer.toString(CharsetUtil.UTF_8));
}
});
}
});
// 根据IP、端口连接Redis做事端
client.connect("192.168.12.129", 6379).sync();
} catch(Exception e){
e.printStackTrace();
}
}
privatestaticByteBuf respCommand(String command){
// 先对传入的命令以空格进行分割
String[] commands = command.split(" ");
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
// 遵照RESP协议:先写入指令的个数
buffer.writeBytes((""+ commands.length).getBytes());
buffer.writeBytes(LINE);
// 接着分别写入每个指令的长度以及详细值
for(String s : commands) {
buffer.writeBytes(("$"+ s.length()).getBytes());
buffer.writeBytes(LINE);
buffer.writeBytes(s.getBytes());
buffer.writeBytes(LINE);
}
// 把转换成RESP格式的命令返回
returnbuffer;
}
}
在上述这个案例中,也仅仅只是通过respCommand()这个方法,对用户输入的指令进行了转换。同时在上面通过Netty,与Redis的地址、端口建立了连接。在连接建立成功后,就会向Redis发送一条转换成RESP指令的set命令。接着等待Redis的相应结果并输出,如下:
+OK
由于这是一条写指令,以是当Redis收到实行完成后,终极就会返回一个OK,大家也可直接去Redis中查询,也依旧能够查询到刚刚写入的name这个键值。
6、HTTP超文本传输协议参考学习前面咱们自己针对付Redis的RESP协议,对用户指令进行了封装,然后发往Redis实行。
但对付这些常用的协议,Netty早已供应好了现成的处理器,想要利用时无需从头开拓,可以直策应用现成的处理器来实现。
比如现在咱们可以基于Netty供应的处理器,实现一个大略的HTTP做事器。
代码如下:
// 基于Netty供应的处理器实现HTTP做事器
publicclassHttpServer {
publicstaticvoidmain(String[] args) throwsInterruptedException {
EventLoopGroup boss = newNioEventLoopGroup();
EventLoopGroup worker = newNioEventLoopGroup();
ServerBootstrap server = newServerBootstrap();
server
.group(boss,worker)
.channel(NioServerSocketChannel.class)
.childHandler(newChannelInitializer<NioSocketChannel>() {
@Override
protectedvoidinitChannel(NioSocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 添加一个Netty供应的HTTP处理器
pipeline.addLast(newHttpServerCodec());
pipeline.addLast(newChannelInboundHandlerAdapter() {
@Override
publicvoidchannelRead(ChannelHandlerContext ctx,
Object msg) throwsException {
// 在这里输出一下的类型
System.out.println("类型:"+ msg.getClass());
super.channelRead(ctx, msg);
}
});
pipeline.addLast(newSimpleChannelInboundHandler<HttpRequest>() {
@Override
protectedvoidchannelRead0(ChannelHandlerContext ctx,
HttpRequest msg) throwsException {
System.out.println("客户真个要求路径:"+ msg.uri());
// 创建一个相应工具,版本号与客户端保持同等,状态码为OK/200
DefaultFullHttpResponse response =
newDefaultFullHttpResponse(
msg.protocolVersion(),
HttpResponseStatus.OK);
// 布局相应内容
byte[] content = "<h1>Hi, ZhuZi!</h1>".getBytes();
// 设置相应头:见告客户端本次相应的数据长度
response.headers().setInt(
HttpHeaderNames.CONTENT_LENGTH,content.length);
// 设置相应主体
response.content().writeBytes(content);
// 向客户端写入相应数据
ctx.writeAndFlush(response);
}
});
}
})
.bind("127.0.0.1",8888)
.sync();
}
}
在该案例中,咱们就未曾手动对HTTP的数据包进行拆包处理了,而是在做事真个pipeline上添加了一个HttpServerCodec处理器,这个处理器是Netty官方供应的。
其类继续关系如下:
publicfinalclassHttpServerCodec
extendsCombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>
implementsSourceCodec {
// ......
}
不雅观察会创造,该类继续自CombinedChannelDuplexHandler这个组合类,它组合了编码器、解码器。
这也就意味着HttpServerCodec即可以对客户真个数据做解码,也可以对做事端相应的数据做编码。
同时除开添加了这个处理器外,在第二个处理器中打印了一下客户真个类型,末了一个处理器中,对客户真个要求做出了相应,实在也便是返回了一句话而已。
此时在浏览器输入http://127.0.0.1:8888/index.html,结果如下:
类型:classio.netty.handler.codec.http.DefaultHttpRequest
类型:classio.netty.handler.codec.http.LastHttpContent$1
客户真个要求路径:/index.html
此时来当作果,客户真个要求会被解析成两个部分:
1)第一个是要求信息;2)第二个是主体信息。但按理来说浏览器发出的要求,属于GET类型的要求,GET要求是没有要求体信息的,但Netty依旧会解析成两部分~,只不过GET要求的第二部分是空的。
在第三个处理器中,咱们直接向客户端返回了一个h1标签,同时也要记得在相应头里面,加上相应内容的长度信息,否则浏览器的加载圈,会一贯不同的迁徙改变,毕竟浏览器也不知道内容有多长,就会一贯反复加载,考试测验等待更多的数据。
7、自定义传输协议7.1概述Netty除开供应了HTTP协议的处理器外,还供应了DNS、HaProxy、MemCache、MQTT、Protobuf、Redis、SCTP、RTSP.....一系列协议的实现,详细定义位于io.netty.handler.codec这个包下,当然,咱们也可以自己实现自定义协议,按照自己的逻辑对数据进行编解码处理。
很多基于Netty开拓的中间件/组件,其内部基本上都开拓了专属的通信协议,以此来作为不同节点间通信的根本,以是解下来咱们基于Netty也来自己设计一款通信协议,这也会作为后续实现谈天程序时的根本。
所谓的协议设计,实在仅仅只须要按照一定约束,实现编码器与解码器即可,发送方在发出数据之前,会经由编码器对数据进行处理,而吸收方在收到数据之前,则会由解码器对数据进行处理。
7.2自定义协议的要素在自定义传输协议时,咱们一定须要考虑几个成分,如下:
1)魔数:用来第一韶光判断是否为自己须要的数据包;2)版本号:提高协议的拓展性,方便后续对协议进行升级;3)序列化算法:正文详细该利用哪种办法进行序列化传输,例如Json、ProtoBuf、JDK...;4)类型:第一韶光判断出当前的类型;5)序号:为了实现双工通信,客户端和做事端之间收/发不会相互壅塞;6)正文长度:供应给LTC解码器利用,防止解码时涌现粘包、半包的征象;7)正文:本次要传输的详细数据。在设计协议时,一个完全的协议该当涵盖上述所说的几方面,这样才能供应双方通信时的根本。
基于上述几个字段,能够在第一韶光内判断出:
1)是否可用;2)当前协议版本;3)的详细类型;4)的长度等各种信息。从而给后续处理器利用(自定义的协议规则本身便是一个编解码处理器而已)。
7.3自定义协议实战前面大略聊到过,所谓的自定义协议便是自己规定格式,以及自己实现编/解码器对实现封装/拆解,以是这里想要自定义一个协议,就只须要知足前面两个条件即可。
因此实现如下:
@ChannelHandler.Sharable
publicclassChatMessageCodec extendsMessageToMessageCodec<ByteBuf, Message> {
// 出站时会经由的编码方法(将原生工具封装成自定义协议的格式)
@Override
protectedvoidencode(ChannelHandlerContext ctx, Message msg,
List<Object> list) throwsException {
ByteBuf outMsg = ctx.alloc().buffer();
// 前五个字节作为魔数
byte[] magicNumber = newbyte[]{'Z','h','u','Z','i'};
outMsg.writeBytes(magicNumber);
// 一个字节作为版本号
outMsg.writeByte(1);
// 一个字节表示序列化办法 0:JDK、1:Json、2:ProtoBuf.....
outMsg.writeByte(0);
// 一个字节用于表示类型
outMsg.writeByte(msg.getMessageType());
// 四个字节表示序号
outMsg.writeInt(msg.getSequenceId());
// 利用Java-Serializable的办法对工具进行序列化
ByteArrayOutputStream bos = newByteArrayOutputStream();
ObjectOutputStream oos = newObjectOutputStream(bos);
oos.writeObject(msg);
byte[] msgBytes = bos.toByteArray();
// 利用四个字节描述正文的长度
outMsg.writeInt(msgBytes.length);
// 将序列化后的工具作为正文
outMsg.writeBytes(msgBytes);
// 将封装好的数据通报给下一个处理器
list.add(outMsg);
}
// 入站时会经由的解码方法(将自定义格式的转变为详细的工具)
@Override
protectedvoiddecode(ChannelHandlerContext ctx,
ByteBuf inMsg, List<Object> list) throwsException {
// 读取前五个字节得到魔数
byte[] magicNumber = newbyte[5];
inMsg.readBytes(magicNumber,0,5);
// 再读取一个字节得到版本号
byteversion = inMsg.readByte();
// 再读取一个字节得到序列化办法
byteserializableType = inMsg.readByte();
// 再读取一个字节得到类型
bytemessageType = inMsg.readByte();
// 再读取四个字节得到序号
intsequenceId = inMsg.readInt();
// 再读取四个字节得到正文长度
intmessageLength = inMsg.readInt();
// 再根据正文长度读取序列化后的字节正文数据
byte[] msgBytes = newbyte[messageLength];
inMsg.readBytes(msgBytes,0,messageLength);
// 对付读取到的正文进行反序列化,终极得到详细的工具
ByteArrayInputStream bis = newByteArrayInputStream(msgBytes);
ObjectInputStream ois = newObjectInputStream(bis);
Message message = (Message) ois.readObject();
// 终极把反序列化得到的工具通报给后续的处理器
list.add(message);
}
}
上面自定义的处理器中,继续了MessageToMessageCodec类,紧张卖力将数据在原生ByteBuf与Message之间进行相互转换,而Message工具是自定义的工具,这里暂且无需过多关心。
个中紧张实现了两个方法:
1)encode():出站时会经由的编码方法,会将原生工具按自定义的协议封装成对应的字节数据;2)decode():入站时会经由的解码方法,会将协议格式的字节数据,转变为详细的工具。上述自定义的协议,也便是一定规则的字节数据,每条数据的组成如下:
1)魔数:利用第1~5个字节来描述,这个魔数值可以按自己的想法自定义;2)版本号:利用第6个字节来描述,不同数字表示不同版本;3)序列化算法:利用第7个字节来描述,不同数字表示不同序列化办法;4)类型:利用第8个字节来描述,不同的类型利用不同数字表示;5)序号:利用第9~12个字节来描述,实在便是一个四字节的整数;6)正文长度:利用第13~16个字节来描述,也是一个四字节的整数;7)正文:长度不固定,根据每次详细发送的数据来决定。在个中,为了实现大略,这里的序列化办法,则采取的是JDK默认的Serializable接口办法,但这种办法天生的工具字节较大,实际情形中最好还是选择谷歌的ProtoBuf办法,这种算法属于序列化算法中,性能最佳的一种落地实现。
当然,这个自定义的协议是供应给后续的谈天业务利用的,但这种实战型的内容分享,基本上代码量较高,以是大家看起来会有些呆板,而本文所利用的谈天室案例,是基于《B站-黑马Netty视频教程》二次改良的,因此如若觉得笔墨描述较为呆板,可直接点击前面给出的链接,不雅观看P101~P121视频进行学习。
末了来不雅观察一下,大家会创造,在咱们定义的这个协议编解码处理器上,存在着一个@ChannelHandler.Sharable表明,这个表明的浸染是干吗的呢?实在很大略,用来标识当前处理器是否可在多线程环境下利用,如果带有该表明的处理器,则表示可以在多个通道间共用,因此只须要创建一个即可,反之同理,如果不带有该表明的处理器,则每个通道须要单独创建利用。
PS:如果你想系统学习Protobuf,可以从以下文章入手:
《如何选择即时通讯运用的数据传输格式》
《强列建议将Protobuf作为你的即时通讯运用数据传输格式》
《IM通讯协议专题学习(一):Protobuf从入门到精通,一篇就够!
》
《IM通讯协议专题学习(二):快速理解Protobuf的背景、事理、利用、优缺陷》
《IM通讯协议专题学习(三):由浅入深,从根上理解Protobuf的编解码事理》
《IM通讯协议专题学习(四):从Base64到Protobuf,详解Protobuf的数据编码事理》
《IM通讯协议专题学习(八):金蝶随手记团队的Protobuf运用实践(事理篇)》
8、实战要点1:IM程序的用户模块8.1概述谈天、谈天,自然是得先有人,然后才能进行谈天沟通。与QQ、微信类似,如果你想要利用某款谈天程序时,条件都得是先具备一个对应的账户才行。
因此在咱们设计IM系统之处,那也须要对应的用户功能实现。但这里为了大略,同样不再结合数据库实现完全的用户模块了,而是基于内存实现用户的管理。
如下:
publicinterfaceUserService {
booleanlogin(String username, String password);
}
这是用户模块的顶层接口,仅仅只供应了一个登录接口,关于注册、鉴权、等级.....等一系列功能,大家感兴趣的可在后续进行拓展实现,接着来看看该接口的实现类。
如下:
publicclassUserServiceMemoryImpl implementsUserService {
privateMap<String, String> allUserMap = newConcurrentHashMap<>();
{
// 在代码块中对用户列表进行初始化,向个中添加了两个用户信息
allUserMap.put("ZhuZi", "123");
allUserMap.put("XiongMao", "123");
}
@Override
publicbooleanlogin(String username, String password) {
String pass = allUserMap.get(username);
if(pass == null) {
returnfalse;
}
returnpass.equals(password);
}
}
这个实现类并未结合数据库来实现,而是仅仅在程序启动时,通过代码块的办法,加载了ZhuZi、XiongMao两个用户信息并放入内存的Map容器中,这里有兴趣的小伙伴,可自行将Map容器换成数据库的表即可。
个中实现的login()登录接口尤为大略,仅仅只是判断了一下有没有对运用户,如果有的话则看看密码是否精确,精确返回true,密码缺点则返回false。是的,我所写的登录功能便是这么大略,走个大略的过场,哈哈哈~
8.2做事端、客户真个根本架构基本的用户模块有了,但这里还未曾套入详细实现,因此先大略的搭建出做事端、客户真个架构,然后再基于构建好的架构实现根本的用户登录功能。
做事真个根本搭建如下:
publicclassChatServer {
publicstaticvoidmain(String[] args) {
NioEventLoopGroup boss = newNioEventLoopGroup();
NioEventLoopGroup worker = newNioEventLoopGroup();
ChatMessageCodec MESSAGE_CODEC = newChatMessageCodec();
try{
ServerBootstrap serverBootstrap = newServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(newChannelInitializer<SocketChannel>() {
@Override
protectedvoidinitChannel(SocketChannel ch) throwsException {
ch.pipeline().addLast(MESSAGE_CODEC);
}
});
Channel channel = serverBootstrap.bind(8888).sync().channel();
channel.closeFuture().sync();
} catch(InterruptedException e) {
System.out.println("做事端涌现缺点:"+ e);
} finally{
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
做事真个代码目前很大略,仅仅只是装载了一个自己的协议编/解码处理器,然后便是一些老步骤,不再过多的重复赘述,接着再来搭建一个大略的客户端。
代码实现如下:
publicclassChatClient {
publicstaticvoidmain(String[] args) {
NioEventLoopGroup group = newNioEventLoopGroup();
ChatMessageCodec MESSAGE_CODEC = newChatMessageCodec();
try{
Bootstrap bootstrap = newBootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(group);
bootstrap.handler(newChannelInitializer<SocketChannel>() {
@Override
protectedvoidinitChannel(SocketChannel ch) throwsException {
ch.pipeline().addLast(MESSAGE_CODEC);
}
});
Channel channel = bootstrap.connect("localhost", 8888).sync().channel();
channel.closeFuture().sync();
} catch(Exception e) {
System.out.println("客户端涌现缺点:"+ e);
} finally{
group.shutdownGracefully();
}
}
}
目前仅仅只是与做事端建立了连接,然后装载了一个自定义的编解码器,到这里就搭建了最基本的做事端、客户真个根本架构,接着来基于它实现大略的登录功能。
8.3用户登录功能的实现对付登录功能,由于须要在做事端与客户端之间传输数据,因此咱们可以设计一个工具,但由于后续单聊、群聊都须要发送不同的格式,因此先设计出一个父类。
如下:
publicabstractclassMessage implementsSerializable {
privateintsequenceId;
privateintmessageType;
@Override
publicString toString() {
return"Message{"+
"sequenceId="+ sequenceId +
", messageType="+ messageType +
'}';
}
publicintgetSequenceId() {
returnsequenceId;
}
publicvoidsetSequenceId(intsequenceId) {
this.sequenceId = sequenceId;
}
publicvoidsetMessageType(intmessageType) {
this.messageType = messageType;
}
publicabstractintgetMessageType();
publicstaticfinalintLoginRequestMessage = 0;
publicstaticfinalintLoginResponseMessage = 1;
publicstaticfinalintChatRequestMessage = 2;
publicstaticfinalintChatResponseMessage = 3;
publicstaticfinalintGroupCreateRequestMessage = 4;
publicstaticfinalintGroupCreateResponseMessage = 5;
publicstaticfinalintGroupJoinRequestMessage = 6;
publicstaticfinalintGroupJoinResponseMessage = 7;
publicstaticfinalintGroupQuitRequestMessage = 8;
publicstaticfinalintGroupQuitResponseMessage = 9;
publicstaticfinalintGroupChatRequestMessage = 10;
publicstaticfinalintGroupChatResponseMessage = 11;
publicstaticfinalintGroupMembersRequestMessage = 12;
publicstaticfinalintGroupMembersResponseMessage = 13;
publicstaticfinalintPingMessage = 14;
publicstaticfinalintPongMessage = 15;
}
在这个父类中,定义了多种类型的状态码,不同的类型对应不同数字,同时个中还设计了一个抽象方法,即getMessageType(),该方法交给详细的子类实现,每个子类返回各自的类型,为了方便后续拓展,这里又创建了一个抽象类作为中间类。
如下:
publicabstractclassAbstractResponseMessage extendsMessage {
privatebooleansuccess;
privateString reason;
publicAbstractResponseMessage() {
}
publicAbstractResponseMessage(booleansuccess, String reason) {
this.success = success;
this.reason = reason;
}
@Override
publicString toString() {
return"AbstractResponseMessage{"+
"success="+ success +
", reason='"+ reason + '\''+
'}';
}
publicbooleanisSuccess() {
returnsuccess;
}
publicvoidsetSuccess(booleansuccess) {
this.success = success;
}
publicString getReason() {
returnreason;
}
publicvoidsetReason(String reason) {
this.reason = reason;
}
}
这个类紧张是供应给相应时利用的,个中包含了相应状态以及相应信息,接着再设计两个登录时会用到的工具。
如下:
publicclassLoginRequestMessage extendsMessage {
privateString username;
privateString password;
publicLoginRequestMessage() {
}
@Override
publicString toString() {
return"LoginRequestMessage{"+
"username='"+ username + '\''+
", password='"+ password + '\''+
'}';
}
publicString getUsername() {
returnusername;
}
publicvoidsetUsername(String username) {
this.username = username;
}
publicString getPassword() {
returnpassword;
}
publicvoidsetPassword(String password) {
this.password = password;
}
publicLoginRequestMessage(String username, String password) {
this.username = username;
this.password = password;
}
@Override
publicintgetMessageType() {
returnLoginRequestMessage;
}
}
上述这个类,紧张是供应给客户端登录时利用,实质上也便是一个涵盖用户名、用户密码的工具而已,同时还有一个用来给做事端相应时的相应类。
如下:
publicclassLoginResponseMessage extendsAbstractResponseMessage {
publicLoginResponseMessage(booleansuccess, String reason) {
super(success, reason);
}
@Override
publicintgetMessageType() {
returnLoginResponseMessage;
}
}
登录相应类的实现十分大略,由登录状态和登录组成,OK,接着来看看登录的详细实现。
首先在客户端中,再通过pipeline添加一个处理器,如下:
CountDownLatch WAIT_FOR_LOGIN = newCountDownLatch(1);
AtomicBoolean LOGIN = newAtomicBoolean(false);
AtomicBoolean EXIT = newAtomicBoolean(false);
Scanner scanner = newScanner(System.in);
ch.pipeline().addLast("client handler", newChannelInboundHandlerAdapter() {
@Override
publicvoidchannelActive(ChannelHandlerContext ctx) throwsException {
// 卖力吸收用户在掌握台的输入,卖力向做事器发送各种
newThread(() -> {
System.out.println("请输入用户名:");
String username = scanner.nextLine();
if(EXIT.get()){
return;
}
System.out.println("请输入密码:");
String password = scanner.nextLine();
if(EXIT.get()){
return;
}
// 布局工具
LoginRequestMessage message = newLoginRequestMessage(username, password);
System.out.println(message);
// 发送
ctx.writeAndFlush(message);
System.out.println("等待后续操作...");
try{
WAIT_FOR_LOGIN.await();
} catch(InterruptedException e) {
e.printStackTrace();
}
// 如果登录失落败
if(!LOGIN.get()) {
ctx.channel().close();
return;
}
}).start();
}
在与做事端建立连接成功之后,就提示用户须要登录,接着吸收用户输入的用户名、密码,然后构建出一个LoginRequestMessage工具,接着将其发送给做事端,由于前面装载了自定义的协议编解码器,以是在出站时,这个Message工具会被序列化成字节码,接着再做事端入站时,又会被反序列化成工具,接着来看看做事真个实现。
如下:
@ChannelHandler.Sharable
publicclassLoginRequestMessageHandler
extendsSimpleChannelInboundHandler<LoginRequestMessage> {
@Override
protectedvoidchannelRead0(ChannelHandlerContext ctx,
LoginRequestMessage msg) throwsException {
String username = msg.getUsername();
String password = msg.getPassword();
booleanlogin = UserServiceFactory.getUserService().login(username, password);
LoginResponseMessage message;
if(login) {
SessionFactory.getSession().bind(ctx.channel(), username);
message = newLoginResponseMessage(true, "登录成功");
} else{
message = newLoginResponseMessage(false, "用户名或密码禁绝确");
}
ctx.writeAndFlush(message);
}
}
在做事端中,新增了一个处理器类,继续自SimpleChannelInboundHandler这个处理器,个中指定的泛型为LoginRequestMessage,这表示当前处理器只关注这个类型的,当涌现登录类型的时,会进入该处理器并触发内部的channelRead0()方法。
在该方法中,获取了登录中的用户名、密码,接着对其做了基本的登录效验,如果用户名存在并且密码精确,就会返回登录成功,否则会返回登录失落败,终极登录后的状态会被封装成一个LoginResponseMessage工具,然后写回客户真个通道中。
当然,为了该处理器能够成功生效,这里须要将其装载到做事真个pipeline上。
如下:
LoginRequestMessageHandler LOGIN_HANDLER = newLoginRequestMessageHandler();
ch.pipeline().addLast(LOGIN_HANDLER);
装载好登录处理器后,接着分别启动做事端、客户端,测试结果如下:
从图中的效果来看,这里实现了最基本的登录功能,估计有些小伙伴看到这里就有些晕了,但实在非常大略,仅仅只是通过Netty在做数据交互而已,客户端则供应输入用户名、密码的功能,然后将用户输入的名称、密码发送给做事端,做事端供应登录判断的功能,终极根据判断结果再向客户端返回数据罢了。
9、实战要点2:实现点对点单聊9.1概述有了基本的用户登录功能后,接着来看看如何实现点对点的单聊功能呢?
首先我定义了一个会话接口,如下:
publicinterfaceSession {
voidbind(Channel channel, String username);
voidunbind(Channel channel);
Channel getChannel(String username);
}
这个接口中依旧只有三个方法,释义如下:
1)bind():传入一个用户名和Socket通道,让两者之间的产生绑定关系;2)unbind():取消一个用户与某个Socket通道的绑定关系;3)getChannel():根据一个用户名,获取与其存在绑定关系的通道。该接口的实现类如下:
publicclassSessionMemoryImpl implementsSession {
privatefinalMap<String, Channel> usernameChannelMap = newConcurrentHashMap<>();
privatefinalMap<Channel, String> channelUsernameMap = newConcurrentHashMap<>();
@Override
publicvoidbind(Channel channel, String username) {
usernameChannelMap.put(username, channel);
channelUsernameMap.put(channel, username);
channelAttributesMap.put(channel, newConcurrentHashMap<>());
}
@Override
publicvoidunbind(Channel channel) {
String username = channelUsernameMap.remove(channel);
usernameChannelMap.remove(username);
channelAttributesMap.remove(channel);
}
@Override
publicChannel getChannel(String username) {
returnusernameChannelMap.get(username);
}
@Override
publicString toString() {
returnusernameChannelMap.toString();
}
}
该实现类最关键的是个中的两个Map容器,usernameChannelMap用来存储所有用户名与Socket通道的绑定关系,而channelUsernameMap则是反过来的顺序,这紧张是为了方便,即可以通过用户名得到对应通道,也可以通过通道判断出用户名,实际上一个Map也能搞定,但还是那句话,紧张为了大略嘛~
有了上述这个最大略的会话管理功能后,就要动手实现详细的功能了,其实在前面实现登录功能的时候,就用过这个中的bind()方法,也便是当登录成功之后,就会将当前发送登录的通道,与正在登录的用户名产生绑定关系,这样就方便后续实现单聊、群聊的功能。
9.2定义单聊的工具与登录时相同,由于须要在做事端和客户端之间实现数据的转发,因此这里也须要两个工具,用来作为数据交互的格式。
如下:
publicclassChatRequestMessage extendsMessage {
privateString content;
privateString to;
privateString from;
publicChatRequestMessage() {
}
publicChatRequestMessage(String from, String to, String content) {
this.from = from;
this.to = to;
this.content = content;
}
// 省略Get/Setting、toString()方法.....
}
上述这个类,是供应给客户端用来发送数据的,个中紧张包含了三个值,谈天的内容、发送人与吸收人。由于这里是须要实现一个IM谈天程序,以是并不是客户端与做事端进行数据交互,而是客户端与客户端之间进行数据交互,做事端仅仅只供应转发的功能,接着再构建一个类。
如下:
publicclassChatResponseMessage extendsAbstractResponseMessage {
privateString from;
privateString content;
@Override
publicString toString() {
return"ChatResponseMessage{"+
"from='"+ from + '\''+
", content='"+ content + '\''+
'}';
}
publicChatResponseMessage(booleansuccess, String reason) {
super(success, reason);
}
publicChatResponseMessage(String from, String content) {
this.from = from;
this.content = content;
}
@Override
publicintgetMessageType() {
returnChatResponseMessage;
}
// 省略Get/Setting、toString()方法.....
}
这个类是供应给做事端用来转发的,当做事端收到一个谈天后,由于谈天中包含了吸收人,以是可以先根据吸收人的用户名,找到对应的客户端通道,然后再封装成一个相应,转发给对应的客户端即可,下面来做详细实现。
9.3实现点对点单聊功能由于谈天功能是供应给客户端利用的,以是当一个客户端登录成功之后,该当暴露给用户一个操作菜单,以是直接在原来客户真个channelActive()方法中,登录成功之后连续加代码即可。
代码如下:
while(true) {
System.out.println("==================================");
System.out.println("\t1、发送单聊");
System.out.println("\t2、发送群聊");
System.out.println("\t3、创建一个群聊");
System.out.println("\t4、获取群聊成员");
System.out.println("\t5、加入一个群聊");
System.out.println("\t6、退出一个群聊");
System.out.println("\t7、退出谈天系统");
System.out.println("==================================");
String command = scanner.nextLine();
}
首先会开启一个去世循环,然后不断吸收用户的操作,接着利用switch语法来对详细的菜单功能进行实现,先实现单聊功能。
如下:
switch(command){
case"1":
System.out.print("请选择你要发送给谁:");
String toUserName = scanner.nextLine();
System.out.print("请输入你要发送的内容:");
String content = scanner.nextLine();
ctx.writeAndFlush(newChatRequestMessage(username, toUserName, content));
break;
}
如果用户选择了单聊,接着会提示用户选择要发送给谁,这里也便是让用户输入对方的用户名,实际上如果有界面的话,这一步是并不须要用户自己输入的,而是供应窗口让用户点击,比如QQ、微信一样,想要给某个人发送时,只须要点击“他”的头像私聊即可。
等用户选择了谈天目标,并且输入了内容后,接着会构建一个ChatRequestMessage工具,然后会发送给做事端,但这里先不看做事真个实现,客户端这边还须要重写一个方法。
如下:
@Override
publicvoidchannelRead(ChannelHandlerContext ctx, Object msg) throwsException {
System.out.println("收到:"+ msg);
if((msg instanceofLoginResponseMessage)) {
LoginResponseMessage response = (LoginResponseMessage) msg;
if(response.isSuccess()) {
// 如果登录成功
LOGIN.set(true);
}
// 唤醒 system in 线程
WAIT_FOR_LOGIN.countDown();
}
}
前面的逻辑是在channelActive()方法中完成的,也便是连接建立成功后,就会让用户登录,接着登录成功之后会给用户一个菜单栏,供应给用户进行操作,但前面的逻辑中一贯没有对做事端相应的进行处理,因此channelRead()方法中会对做事端相应的数据进行处理。
channelRead()方法会在有数据可读时被触发,以是当做事端相应数据时,首先会判断一下:目前做事端相应的是不是登录,如果是的话,则须要根据登录的结果来唤醒前面channelActive()方法中的线程。如果目前做事端相应的不是登录,这也就意味着客户端前面已经登录成功了,以是接着会直接打印一下收到的数据。
OK,有了上述客户真个代码实现后,接着再来做事端多创建一个处理器。
如下:
@ChannelHandler.Sharable
publicclassChatRequestMessageHandler
extendsSimpleChannelInboundHandler<ChatRequestMessage> {
@Override
protectedvoidchannelRead0(ChannelHandlerContext ctx,
ChatRequestMessage msg) throwsException {
String to = msg.getTo();
Channel channel = SessionFactory.getSession().getChannel(to);
// 在线
if(channel != null) {
channel.writeAndFlush(newChatResponseMessage(
msg.getFrom(), msg.getContent()));
}
// 不在线
else{
ctx.writeAndFlush(newChatResponseMessage(
false, "对方用户不存在或者不在线"));
}
}
}
这里依旧通过继续SimpleChannelInboundHandler类的形式,来特殊关注ChatRequestMessage单聊类型的,如果目前做事端收到的是单聊,则会进入触发该处理器的channelRead0()方法。
该处理器内部的逻辑也并不繁芜,首先根据单聊的吸收人,去找一下与之对应的通道:
1)如果根据用户名查到了通道,表示吸收人目前是登录在线状态;2)反之,如果无法根据用户名找到通道,表示对应的用户不存在或者没有登录。接着会根据上面的查询结果,进行对应的结果返回:
1)如果在线:把要发送的单聊,直接写入至找到的通道中;2)如果不在线:向发送单聊的客户端,返回用户不存在或用户不在线。有了这个处理器之后,接着还须要把该处理器装载到做事端上,如下:
ChatRequestMessageHandler CHAT_HANDLER = newChatRequestMessageHandler();
ch.pipeline().addLast(CHAT_HANDLER);
装载好单聊处理器后,接着分别启动一个做事端、两个客户端,测试结果如下:
从测试结果中可以明显看出效果,个中的单聊功能的确已经实现,可以实现A→B用户之间的单聊功能,两者之间借助做事器转发,可以实现两人私聊的功能。
10、实战要点3:打造多人谈天室10.1概述前面实现了两个用户之间的私聊功能,接着再来实现一个多人谈天室的功能,毕竟像QQ、微信、钉钉....等任何通讯软件,都支持多人建立群聊的功能。
但多人谈天室的功能,实现之前还须要先完成建群的功能,毕竟如果群都没建立,自然无法向某个群内发送数据。
实现拉群也好,群聊也罢,实在现步骤依旧和前面相同,如下:
1)先定义对应的工具;2)实现客户端发送对应数据的功能;3)再写一个做事真个群聊处理器,然后装载到做事端上。10.2定义拉群的体首先来定义两个拉群时用的体,如下:
publicclassGroupCreateRequestMessage extendsMessage {
privateString groupName;
privateSet<String> members;
publicGroupCreateRequestMessage(String groupName, Set<String> members) {
this.groupName = groupName;
this.members = members;
}
@Override
publicintgetMessageType() {
returnGroupCreateRequestMessage;
}
// 省略其他Get/Settings、toString()方法.....
}
上述这个别是供应给客户端利用的,个中紧张存在两个成员,也便是群名称与群成员列表,存放所有群成员的容器选用了Set凑集,由于Set凑集具备不可重复性,因此可以有效的避免同一用户多次进群,接着再来看看做事端相应时用的体。
如下:
publicclassGroupCreateResponseMessage extendsAbstractResponseMessage {
publicGroupCreateResponseMessage(booleansuccess, String reason) {
super(success, reason);
}
@Override
publicintgetMessageType() {
returnGroupCreateResponseMessage;
}
}
这个别的实现尤为大略,仅仅只是给客户端返回了拉群状态以及拉群的附加信息。
10.3定义群聊会话管理前面单聊有单聊的会话管理机制,而实现多人群聊时,依旧须要有群聊的会话管理机制,首先封装了一个群聊实体类。
如下:
publicclassGroup {
// 谈天室名称
privateString name;
// 谈天室成员
privateSet<String> members;
publicstaticfinalGroup EMPTY_GROUP = newGroup("empty", Collections.emptySet());
publicGroup(String name, Set<String> members) {
this.name = name;
this.members = members;
}
// 省略其他Get/Settings、toString()方法.....
}
接着定义了一个群聊会话的顶级接口,如下:
publicinterfaceGroupSession {
// 创建一个群聊
Group createGroup(String name, Set<String> members);
// 加入某个群聊
Group joinMember(String name, String member);
// 移除群聊中的某个成员
Group removeMember(String name, String member);
// 终结一个群聊
Group removeGroup(String name);
// 获取一个群聊的成员列表
Set<String> getMembers(String name);
// 获取一个群聊所有在线用户的Channel通道
List<Channel> getMembersChannel(String name);
}
上述接口中,供应了几个接口方法,实在也紧张是群聊系统中的一些日常操作,如创群、加群、踢人、终结群、查看群成员....等功能,接着来看看该接口的实现者。
如下:
publicclassGroupSessionMemoryImpl implementsGroupSession {
privatefinalMap<String, Group> groupMap = newConcurrentHashMap<>();
@Override
publicGroup createGroup(String name, Set<String> members) {
Group group = newGroup(name, members);
returngroupMap.putIfAbsent(name, group);
}
@Override
publicGroup joinMember(String name, String member) {
returngroupMap.computeIfPresent(name, (key, value) -> {
value.getMembers().add(member);
returnvalue;
});
}
@Override
publicGroup removeMember(String name, String member) {
returngroupMap.computeIfPresent(name, (key, value) -> {
value.getMembers().remove(member);
returnvalue;
});
}
@Override
publicGroup removeGroup(String name) {
returngroupMap.remove(name);
}
@Override
publicSet<String> getMembers(String name) {
returngroupMap.getOrDefault(name, Group.EMPTY_GROUP).getMembers();
}
@Override
publicList<Channel> getMembersChannel(String name) {
returngetMembers(name).stream()
.map(member -> SessionFactory.getSession().getChannel(member))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
这个实现类没啥好说的,重点记住里面有个Map容器即可,这个容器紧张卖力存储所有群名称与Group群聊工具的关系,后续可以通过群聊名称,在这个容器中找到一个对应群聊工具。同时为了方便后续调用这些接口,还供应了一个工具类。
如下:
publicabstractclassGroupSessionFactory {
privatestaticGroupSession session = newGroupSessionMemoryImpl();
publicstaticGroupSession getGroupSession() {
returnsession;
}
}
很大略,仅仅只实例化了一个群聊会话管理的实现类,由于这里没有结合Spring来实现,以是并不能依赖IOC技能来自动管理Bean,因此咱们须要手动创建出一个实例,以供于后续利用。
10.4实现拉群功能前面客户真个功能菜单中,3对应着拉群功能,以是咱们须要对3做详细的功能实现。
逻辑如下:
case"3":
System.out.print("请输入你要创建的群聊昵称:");
String newGroupName = scanner.nextLine();
System.out.print("请选择你要约请的群成员(不同成员用、分割):");
String members = scanner.nextLine();
Set<String> memberSet = newHashSet<>(Arrays.asList(members.split("、")));
memberSet.add(username); // 加入自己
ctx.writeAndFlush(newGroupCreateRequestMessage(newGroupName, memberSet));
break;
在该分支实现中,首先会哀求用户输入一个群聊昵称,接着须要输入须要拉入群聊的用户名称,多个用户之间利用、分割,接着会把用户输入的群成员以及自己,全部放入到一个Set凑集中,终极组装成一个拉群体,发送给做事端处理。
做事真个处理器如下:
@ChannelHandler.Sharable
publicclassGroupCreateRequestMessageHandler
extendsSimpleChannelInboundHandler<GroupCreateRequestMessage> {
@Override
protectedvoidchannelRead0(ChannelHandlerContext ctx,
GroupCreateRequestMessage msg) throwsException {
String groupName = msg.getGroupName();
Set<String> members = msg.getMembers();
// 群管理器
GroupSession groupSession = GroupSessionFactory.getGroupSession();
Group group = groupSession.createGroup(groupName, members);
if(group == null) {
// 发生成功
ctx.writeAndFlush(newGroupCreateResponseMessage(true,
groupName + "创建成功"));
// 发送拉群
List<Channel> channels = groupSession.getMembersChannel(groupName);
for(Channel channel : channels) {
channel.writeAndFlush(newGroupCreateResponseMessage(
true, "您已被拉入"+ groupName));
}
} else{
ctx.writeAndFlush(newGroupCreateResponseMessage(
false, groupName + "已经存在"));
}
}
}
这里依旧继续了SimpleChannelInboundHandler类,只关心拉群的,当客户端涌现拉群时,首先会获取用户输入的群昵称和群成员,接着通过前面供应的创群接口,考试测验创建一个群聊,如果群聊已经存在,则会创建失落败,反之则会创建成功,在创建群聊成功的情形下,会给所有的群成员发送一条“你已被拉入[XXX]”的。
末了,同样须要将该处理器装载到做事端上,如下:
GroupCreateRequestMessageHandler GROUP_CREATE_HANDLER =
newGroupCreateRequestMessageHandler();
ch.pipeline().addLast(GROUP_CREATE_HANDLER);
末了分别启动一个做事端、两个客户端进行效果测试,如下:
10.5定义群聊的体
这里就不重复赘述了,还是之前的套路,定义一个客户端用的体,如下:
publicclassGroupChatRequestMessage extendsMessage {
privateString content;
privateString groupName;
privateString from;
publicGroupChatRequestMessage(String from, String groupName, String content) {
this.content = content;
this.groupName = groupName;
this.from = from;
}
@Override
publicintgetMessageType() {
returnGroupChatRequestMessage;
}
// 省略其他Get/Settings、toString()方法.....
}
这个是客户端用来发送群聊的体,个中存在三个成员,发送人、群聊昵称、内容,通过这三个成员,可以描述清楚任何一条群聊记录,接着来看看做事端相应时用的体。
如下:
publicclassGroupChatResponseMessage extendsAbstractResponseMessage {
privateString from;
privateString content;
publicGroupChatResponseMessage(booleansuccess, String reason) {
super(success, reason);
}
publicGroupChatResponseMessage(String from, String content) {
this.from = from;
this.content = content;
}
@Override
publicintgetMessageType() {
returnGroupChatResponseMessage;
}
// 省略其他Get/Settings、toString()方法.....
}
在这个别中,就省去了群聊昵称这个成员,由于这个别的用途,紧张是给做事端转发给客户端时利用的,因此不须要群聊昵称,当然,要也可以,我这里就直接省去了。
10.6实现群聊功能依旧先来做客户真个实现,实现了客户端之后再去完成做事真个实现,客户端实现如下:
case"2":
System.out.print("请选择你要发送的群聊:");
String groupName = scanner.nextLine();
System.out.print("请输入你要发送的内容:");
String groupContent = scanner.nextLine();
ctx.writeAndFlush(newGroupChatRequestMessage(username, groupName, groupContent));
break;
由于发送群聊对应着之前菜单中的2,以是这里对该分支进行实现,当用户选择发送群聊时,首先会让用户自己先选择一个群聊,接着输入要发送的内容,接着组装成一个群聊工具,发送给做事端处理。
做事真个实现如下:
@ChannelHandler.Sharable
publicclassGroupChatRequestMessageHandler
extendsSimpleChannelInboundHandler<GroupChatRequestMessage> {
@Override
protectedvoidchannelRead0(ChannelHandlerContext ctx,
GroupChatRequestMessage msg) throwsException {
List<Channel> channels = GroupSessionFactory.getGroupSession()
.getMembersChannel(msg.getGroupName());
for(Channel channel : channels) {
channel.writeAndFlush(newGroupChatResponseMessage(
msg.getFrom(), msg.getContent()));
}
}
}
这里依旧定义了一个处理器,关于缘故原由就不再重复啰嗦了,做事端对付群聊的实现额外大略,也便是先根据用户选择的群昵称,找到该群所有的群成员,然后依次遍历成员列表,获取对应的Socket通道,转发即可。
接着将该处理器装载到做事端pipeline上,然后分别启动一个做事端、两个客户端,进行效果测试,如下:
效果如上图的注释,基于上述的代码测试,效果确实达到了咱们须要的群聊效果~
10.7谈天室的其他功能实现到这里为止,实现了最基本的建群、群聊的功能,但对付踢人、加群、终结群....等一系列群聊功能还未曾实现,但我这里就不连续重复了。
毕竟还是那个套路:
1)定义对应功能的体;2)客户端向做事端发送对应格式的;3)做事端编写处理器,对特定的进行处理。以是大家感兴趣的情形下,可以根据上述步骤连续进行实现,实现的过程没有任何难度,重点便是韶光问题罢了。
11、本文小结看到这里,实在Netty实战篇的内容也就大致结束了,个人对付实战篇的内容并不怎么满意,由于与最初设想的实现存在很大偏差,这是由于近期事情、生活状态不对,以是内容输出也没那么夯实,对付这篇中的完全代码实现,也包括前面两篇中的一些代码实现(详见“2、配套源码”),大家感兴趣可以自行Down下去玩玩。
在我所撰写的案例中,自定义协议可以连续优化,选择性能更强的序列化办法,而谈天室也可以进一步拓展,比如将用户信息、群聊信息、联系人信息都结合数据库实现,进一步实现离线功能,但由于该案例的设计之初就有问题,所以是存在性能问题的,想要打造一款真正高性能的IM程序,那诸位可参考本系列前面的文章即可。
12、系列文章《随着源码学IM(一):手把手教你用Netty实现心跳机制、断线重连机制》
《随着源码学IM(二):自已开拓IM很难?手把手教你撸一个Andriod版IM》
《随着源码学IM(三):基于Netty,从零开拓一个IM做事端》
《随着源码学IM(四):拿起键盘便是干,教你徒手开拓一套分布式IM系统》
《随着源码学IM(五):精确理解IM长连接、心跳及重连机制,并动手实现》
《随着源码学IM(六):手把手教你用Go快速搭建高性能、可扩展的IM系统》
《随着源码学IM(七):手把手教你用WebSocket打造Web端IM谈天》
《随着源码学IM(八):万字长文,手把手教你用Netty打造IM谈天》
《随着源码学IM(九):基于Netty实现一套分布式IM系统》
《随着源码学IM(十):基于Netty,搭建高性能IM集群(含技能思路+源码)》
《随着源码学IM(十一):一套基于Netty的分布式高可用IM详细设计与实现(有源码)》
《随着源码学IM(十二):基于Netty打造一款高性能的IM即时通讯程序》( 本文)
《SpringBoot集成开源IM框架MobileIMSDK,实现即时通讯IM谈天功能》
13、参考资料[1] 浅谈IM系统的架构设计
[2] 简述移动端IM开拓的那些坑:架构设计、通信协议和客户端
[3] 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)
[4] 一套原创分布式即时通讯(IM)系统理论架构方案
[5] 一套亿级用户的IM架构技能干货(上篇):整体架构、做事拆分等
[6] 一套亿级用户的IM架构技能干货(下篇):可靠性、有序性、弱网优化等
[7] 史上最普通Netty框架入门长文:基本先容、环境搭建、动手实战
[8] 强列建议将Protobuf作为你的即时通讯运用数据传输格式
[9] IM通讯协议专题学习(一):Protobuf从入门到精通,一篇就够!
[10] 融云技能分享:全面揭秘亿级IM的可靠投递机制
[11] IM群聊如此繁芜,如何担保不丢不重?
[12] 零根本IM开拓入门(四):什么是IM系统的时序同等性?
[13] 如何担保IM实时的“时序性”与“同等性”?
[14] 微信的海量IM谈天序列号天生实践(算法事理篇)
[15] 网易云信技能分享:IM中的万人群聊技能方案实践总结
[16] 融云IM技能分享:万人群聊投递方案的思考和实践
[17] 为何基于TCP协议的移动端IM仍旧须要心跳保活机制?
[18] 一文读懂即时通讯运用中的网络心跳包机制:浸染、事理、实现思路等
[19] 微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)
[20] 融云技能分享:融云安卓端IM产品的网络链路保活技能实践
[21] 彻底搞懂TCP协议层的KeepAlive保活机制
[22] 深度解密钉钉即时做事DTIM的技能设计
技能互换:
- 移动端IM开拓入门文章:《新手入门一篇就够:从零开拓移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)
(本文已同步发布于:http://www.52im.net/thread-4530-1-1.html)