This article is shared by the author "Chinese Cabbage", and there are many revisions and changes. Note: This series is an article for IM beginners, IM old fritters still look to Haihan, don't spray!
1 Introduction
Continued from the previous article "IM System Design", this article mainly explains the realization of the single chat function of IM through actual combat coding, and the content involves technical principles and coding practices.
Supplementary note: Because the main purpose of this series of articles is to guide IM beginners to write the logic and thinking ability of IM from scratch step by step in the case of Netty, so in order to simplify the coding implementation, the clients implemented by coding in this series are all It's console-based (hopefully not dismissed), because understanding the nature of the technology is clearly more important than the cool appearance.
study Exchange:
- Introductory article on mobile IM development: "One entry is enough for beginners: developing mobile IM from scratch"
- Open source IM framework source code: https://github.com/JackJiang2011/MobileIMSDK (click here for alternate address)
(This article has been published simultaneously at: http://www.52im.net/thread-3974-1-1.html )
2. Write in front
It is recommended that before reading this article, you must read the first article in this series, "IM System Design". After focusing on understanding the theoretical design ideas of the IM system, it is better to read the actual combat code.
Finally, before starting this article, please be sure to understand the basic knowledge of Netty in advance, starting from the "Knowledge Preparation" chapter in the first "IM System Design" in this series.
3. Series of articles
This article is the second in a series of articles, the following is the series table of contents:
"Based on Netty, developing IM from scratch (1): IM system design"
"Based on Netty, developing IM from scratch (2): coding practice (single chat function)" (* this article)
"Based on Netty, Developing IM from Scratch (3): Coding Practice (Group Chat Function)" (to be released later.. )
"Based on Netty, Developing IM from Scratch (4): Coding Practice (System Optimization)" (to be released later.. )
4. Operation effect
In this article, we mainly implement the IM single chat function, specifically: two users who simulate IM chat log in to their respective accounts, and then can send chat messages to each other.
Let's take a look at the function operation effect to be implemented in this article in advance.
Client 1 login effect:
Client 2 login effect:
Client 1 sends a message renderings:
Client 2 accepts message renderings:
5. Technical principle
5.1 Overview In the previous section, you can see that the operation effect of this actual combat is a chat based on the console console. According to the idea design of the previous article "IM System Design", we also know that the main core is that the server saves a relationship mapping , find the corresponding channel through the recipient ID to send the message.
However, if we want to achieve specific functions, we need to generally disassemble the core technology implementation steps, which are divided into the following three steps.
5.2 The first step: Implementation of encoding and decoding For the IM single chat function, there are two core technical points:
1) One is serialization and deserialization;
2) The second is the realization of the communication protocol.
For the data communication between the client and the server, we interact based on entity objects, so the data format is more convenient.
For the serialization and deserialization of entity objects, it is recommended to use the Fastjson framework instead of the object stream used by the official Netty example.
At the same time, in order to manage different business entities in a more standardized way, we need to define an entity base class, which all business entities inherit (the following chapters will explain in detail).
5.3 The second step: Implementation of two business points: login and message sending Login is mainly to bind the user ID and the channel (that is, the Channel in Netty, that is, the network connection).
After the login is successful, bind the user ID for the Channel through the attr() method. There are three main purposes:
1) When client A sends a message, the server can obtain the user ID of the message sender through Channel, so as to know who sent the message;
2) When the server receives a message from client A, it can obtain the receiver's Channel through the receiver's user ID in the message, so as to know "who" the message should be sent to;
3) When the Channel is disconnected, the server can monitor the Channel and obtain the properties of the Channel, thereby deleting the corresponding user ID and Chennel mapping relationship.
For business processing, user login and message sending are two different business points. Generally speaking, multiple Handlers need to be defined to handle them separately, but here, in order to reduce the number of Handlers, one Handler is unified for processing.
- Friendly reminder: For the binding of user ID and Chennel, you can refer to the implementation logic OnlineProcessor.java in the mature open-source IM project MobileIMSDK, so as to learn through IM product-level practice.
5.4 The third step: The implementation of the mapping relationship has also been analyzed before. The server needs to save a mapping relationship between the user ID and the Channel. This mapping relationship only needs to be stored in a Map, that is, Map<Integer,Channel>, where : The key is the user ID, and the value is the Channel (Channel is the network connection object of the client).
There is not much to explain in this part, and it is enough to understand the relationship between user ID and Channel.
Next is the specific coding practice. . .
6. Entity definition practice
The design of the entity is mainly considered from two aspects:
1) The rules of the communication protocol are four parts: protocol identifier, business instruction, data length, and data;
2) Different services correspond to different field attributes.
Specifically as shown in the figure below:
Base entity:
It mainly defines the tag field, the identifier of the identification protocol, and the code () abstract method, which mainly represents business instructions, and different services correspond to different instructions.
@Data
public abstract class BaseBean implements Serializable {
private Integer tag=1;//固定值,标识的是一个协议类型,不同协议对应不同的值
public abstract Byte code();//业务指令抽象方法
}
Login request entity:
@Data
public class LoginReqBean extends BaseBean implements Serializable {
private Integer userid;//用户ID
private String username;//用户名称
public Byte code() {
return 1;//业务指令
}
}
Login response entity:
@Data
public class LoginResBean extends BaseBean implements Serializable {
private Integer status;//响应状态,0登录成功,1登录失败
private String msg;//响应信息
private Integer userid;//用户ID
public Byte code() {
return 2;//业务指令
}
}
Message sending entity:
public class MsgReqBean extends BaseBean implements Serializable {
private Integer fromuserid;//发送人ID
private Integer touserid;//接受人ID
private String msg;//发送消息
public Byte code() {
return 3;//业务指令
}
}
Message Response Response:
public class MsgResBean extends BaseBean implements Serializable {
private Integer status;//响应状态,0发送成功,1发送失败
private String msg;//响应信息
public Byte code() {
return 4;//业务指令
}
}
Message receiving entity:
public class MsgRecBean extends BaseBean implements Serializable {
private Integer fromuserid;//发送人ID
private String msg;//消息
public Byte code() {
return 5;//业务指令
}
}
7. Encoding and decoding actual combat
7.1 Dependent coordinates
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
7.2 Coding Implementation
public class MyEncoder extends MessageToByteEncoder<BaseBean> {
protected void encode(
ChannelHandlerContext channelHandlerContext,
BaseBean baseBean,
ByteBuf byteBuf) throws Exception {
//1.把实体序列化成字节数字
byte[] bytes= JSON.toJSONBytes(baseBean);
//2.根据协议组装数据
byteBuf.writeInt(baseBean.getTag());//标识(4个字节)
byteBuf.writeByte(baseBean.code());//指令(1个字节)
byteBuf.writeInt(bytes.length);//长度(4个字节)
byteBuf.writeBytes(bytes);//
}
}
7.3 Decoding Implementation
public class MyDecoder extends ByteToMessageDecoder {
protected void decode(
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf,
List<Object> list) throws Exception {
//1.根据协议取出数据
int tag=byteBuf.readInt();//标识符
byte code=byteBuf.readByte();//获取指令
int len=byteBuf.readInt();//获取数据长度
byte[] bytes=new byte[len];
byteBuf.readBytes(bytes);
//2.根据code获取类型
Class<? extendsBaseBean> c= MapUtils.getBean(code);
//3.反序列化
BaseBean baseBean=JSON.parseObject(bytes,c);
list.add(baseBean);
}
}
7.4 Why do commands and entity relationships need such a utility class? Instructions represent business types, and different businesses correspond to different entities. When decoding, how do you know what kind of entities are deserialized? The idea is to obtain the instruction, and then find the corresponding entity according to the instruction.
public class MapUtils {
//1. 自定义指令
private static Byte codeLoginReq=1;
private static Byte codeLoginRes=2;
private static Byte codeMsgReq=3;
private static Byte codeMsgRes=4;
private static Byte codeMsgRec=5;
//2. 自定义一个Map,专门管理指令和实体的关系
private static Map<Byte, Class<? extends BaseBean>> map=new HashMap<Byte,Class<? extends BaseBean>>();
//3. 初始化
static{
map.put(codeLoginReq, LoginReqBean.class);
map.put(codeLoginRes, LoginResBean.class);
map.put(codeMsgReq, MsgReqBean.class);
map.put(codeMsgRes, MsgResBean.class);
map.put(codeMsgRec, MsgRecBean.class);
}
//4. 根据指令获取对应的实体
public static Class<? extends BaseBean> getBean(Byte code){
try{
return map.get(code);
}catch(Exception e){
throw new RuntimeException(e.getMessage());
}
}
}
8. Client-side code combat
8.1 Pipeline management linked list
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
//1.拆包器
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,5,4));
//2.自定义解码器
ch.pipeline().addLast(new MyDecoder());
//3.自定义业务
ch.pipeline().addLast(new ClientChatHandler());
//4.自定义编码器
ch.pipeline().addLast(new MyEncoder());
}
});
8.2 Business Handler
All business processes are processed in the same Handler, and different business processes are distinguished by judging the entity type.
public class ClientChatHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//通道就绪时,发起登录请求
login(ctx.channel());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//根据msg做类型判断,不同的业务做不同的处理
if(msg instanceof LoginResBean){
//1.登录结果响应
LoginResBean res=(LoginResBean) msg;
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>登录响应:"+res.getMsg());
if(res.getStatus()==0){
//1.登录成功,则给通道绑定属性
ctx.channel().attr(AttributeKey.valueOf("userid")).set(res.getUserid());
//2.调用发送消息方法
sendMsg(ctx.channel());
}else{
//1.登录失败,调用登录方法
login(ctx.channel());
}
}elseif(msg instanceof MsgResBean){
//1.发送消息结果响应
MsgResBean res=(MsgResBean)msg;
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>发送响应:"+res.getMsg());
}else if(msg instanceof MsgRecBean){
//2.接受消息
MsgRecBean res=(MsgRecBean)msg;
System.out.println("fromuserid="+res.getFromuserid()+",msg="+res.getMsg());
}
}
//登录方法
private void login(Channel channel){
Scanner scanner=new Scanner(System.in);
System.out.println(">>用户ID:");
Integer userid=scanner.nextInt();
System.out.println(">>用户名称:");
String username=scanner.next();
LoginReqBean bean=new LoginReqBean();
bean.setUserid(userid);
bean.setUsername(username);
channel.writeAndFlush(bean);
}
//发送消息方法
private void sendMsg(finalChannel channel){
final Scanner scanner=new Scanner(System.in);
new Thread(new Runnable() {
public void run() {
while(true){
System.out.println(">>接收人ID:");
Integer touserid=scanner.nextInt();
System.out.println(">>聊天内容:");
String msg=scanner.next();
MsgReqBean bean=new MsgReqBean();
//从通道属性获取发送人ID
Integer fromuserid=(Integer) channel.attr(
AttributeKey.valueOf("userid")
).get();
//发送人ID
bean.setFromuserid(fromuserid);
//接受人ID
bean.setTouserid(touserid);
//发送消息
bean.setMsg(msg);
channel.writeAndFlush(bean);
}
}
}).start();
}
}
9. Server-side code combat
9.1 Pipeline management linked list
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
//1.拆包器
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,5,4));
//2.自定义解码器
ch.pipeline().addLast(new MyDecoder());
//3.业务Handler
ch.pipeline().addLast(new ServerChatHandler());
//4.自定义编码器
ch.pipeline().addLast(new MyEncoder());
}
});
9.2 Business Handler
public class ServerChatHandler extends ChannelInboundHandlerAdapter{
//1.定义一个Map(key是用户ID,value是连接通道)
private static Map<Integer, Channel> map=new HashMap<Integer, Channel>();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof LoginReqBean){
//1.登录请求
login((LoginReqBean) msg,ctx.channel());
}else if(msg instanceof MsgReqBean){
//2.发送消息请求
sendMsg((MsgReqBean)msg,ctx.channel());
}
}
//登录处理方法
private void login(LoginReqBean bean, Channel channel){
LoginResBean res=new LoginResBean();
//从map里面根据用户ID获取连接通道
Channel c=map.get(bean.getUserid());
if(c==null){
//通道为空,证明该用户没有在线
//1.添加到map
map.put(bean.getUserid(),channel);
//2.给通道赋值
channel.attr(AttributeKey.valueOf("userid")).set(bean.getUserid());
//3.响应
res.setStatus(0);
res.setMsg("登录成功");
res.setUserid(bean.getUserid());
channel.writeAndFlush(res);
}else{
//通道不为空,证明该用户已经在线了
res.setStatus(1);
res.setMsg("该账户目前在线");
channel.writeAndFlush(res);
}
}
//消息发送处理方法
private void sendMsg(MsgReqBean bean,Channel channel){
Integer touserid=bean.getTouserid();
Channel c=map.get(touserid);
if(c==null){
MsgResBean res=new MsgResBean();
res.setStatus(1);
res.setMsg(touserid+",不在线");
channel.writeAndFlush(res);
}else{
MsgRecBean res=new MsgRecBean();
res.setFromuserid(bean.getFromuserid());
res.setMsg(bean.getMsg());
c.writeAndFlush(res);
}
}
}
10. Summary of this article
This article mainly codes and combats the single chat function of IM, and the realization idea is relatively small and complicated.
The main core of everyone is to grasp the following ideas:
1) Different businesses can set different entities and instructions, and the relationship between instructions and entities needs to be managed, so that when deserializing is convenient, it can be deserialized to specific entities according to the instructions;
2) It is necessary to manage the relationship between the user ID and the channel (it is convenient to find the Channel channel according to the user ID, and vice versa), and flexibly use the attr () method of the Channel, which can bind the attribute value, which is very useful.
11. References
[1] Teach you to use Netty to implement the heartbeat mechanism, disconnection and reconnection mechanism
[2] Is it difficult to develop IM by yourself? Teach you how to play an Android version of IM
[3] Based on Netty, develop an IM server from scratch
[4] Pick up the keyboard and do it, teach you to develop a distributed IM system with your bare hands
[5] Correctly understand the IM long connection, heartbeat and reconnection mechanism, and implement it
[6] Teach you to quickly build a high-performance and scalable IM system with Go
[7] Teach you to use WebSocket to create web-side IM chat
[8] 4D long text, teach you how to use Netty to create IM chat
[9] Implement a distributed IM system based on Netty
[10] Based on Netty, build a high-performance IM cluster (including technical ideas + source code)
[11] SpringBoot integrates the open source IM framework MobileIMSDK to realize the instant messaging IM chat function (this article has been published simultaneously at: http://www.52im.net/thread-3974-1-1.html )
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。