Welcome to reprint, reprint please indicate the source: juejin.cn/post/684490…

Writing in the front

I always wanted to write an article about im sharing, but I was too busy at work to find the time. Today I resigned from the company and planned to have a good rest for a few days before looking for a new job. Taking advantage of the free time, I decided to settle down and write an article. After all, I have learned a lot from my predecessors. Work five and a half years, the last three or four years have been doing social related project, there are live, instant messaging, short video sharing, community BBS, and other products, know the importance of instant communication technology in a project, based on the spirit of open source share, also take this opportunity to sum up, so I wrote this article, the article has not welcome criticism and corrections.

This article will introduce:

  • Protobuf serialization
  • TCP packet unpacking and packet sticking
  • Long-link handshake authentication
  • heartbeat
  • Reconnection mechanism
  • Message retransmission mechanism
  • Read/write timeout mechanism
  • Offline message
  • The thread pool
  • AIDL communicates across processes

I would like to spend part of the time to introduce the use of AIDL to achieve multi-process communication, improve the application viability, but this method has been ineffective in most of the new versions of Android, and it is more complicated, so AFTER consideration, the part of AIDL can be removed, if you need to know more about my private message.

Take a look at the results first:

If you don’t want to read the article, go to Github fork

Next, let’s get down to business.


Why use TCP?

The difference between TCP/UDP/ WebSocket needs to be explained briefly. The advantages and disadvantages of TCP/UDP are explained here, and the applicable scenarios are summarized briefly:

  • Advantages:

    • The advantages of TCP are stable and reliable. Before data transmission, there are three handshakes to establish a connection, and during data transmission, there are confirmation, window, retransmission, congestion control mechanisms. After data transmission, the connection is disconnected to save system resources.
    • The advantages of UDP are reflected in fast, slightly safer than TCP, UDP does not have the various mechanisms of TCP, is a stateless transmission protocol, so the transfer of data is very fast, without these mechanisms of TCP, the mechanism used by the attack is less, but also can not avoid being attacked.
  • Disadvantages:

    • TCP disadvantages are slow, low efficiency, high occupation of system resources, vulnerable to attack, TCP before the transfer of data to establish a connection, this will consume time, and in the data transfer, confirmation mechanism, retransmission mechanism, congestion mechanism will consume a lot of time, and to maintain all transmission connections on each device.
    • UDP disadvantages are not reliable, unstable, because there is no TCP mechanism, UDP data transmission, if the network quality is not good, it will be easy to lose packets, resulting in data loss.
  • Applicable scenarios:

    • TCP: a file transfer protocol such as HTTP, HTTPS, and FTP, and a mail transfer protocol such as POP and SMTP.
    • UDP: high network communication speed is required when the quality of network communication is not high.

As for Websockets, there will probably be a follow-up article about them. In summary, the decision to use TCP protocol.


Why use Protobuf?

Json/XML /protobuf for App network transmission protocols, there are three common and optional formats, respectively, json/ XML /protobuf.

  • Advantages:

    • The advantage of JSON is that it is much smaller than XML format, with much higher transmission efficiency and good readability.
    • XML has the advantage of being readable and easy to parse.
    • The advantages of Protobuf are fast transfer efficiency (said to be 10-20 times faster than XML and JSON when data is large), small serialization size compared to JSON and XML, cross-platform multi-language support, good message format upgrade and compatibility, and fast serialization and deserialization.
  • Disadvantages:

    • The downside of JSON is that it is not particularly efficient (faster than XML, but much slower than Protobuf).
    • The disadvantage of XML is that it is inefficient and consumes too much resources.
    • The disadvantage of protobuf is that it is not very convenient to use.

In a scenario that requires a large amount of data transmission, selecting a Protobuf can significantly reduce the amount of data and network IO, thereby reducing the time consumed by network transmission. Protobuf is a good choice, considering that as a social product, the amount of message data can be very large, and in order to save traffic.


Why use Netty?

First, let’s take a look at what Netty is. Netty is an open source Framework based on Java NIO provided by JBOSS. Netty provides asynchronous, non-blocking, event-driven, high-performance, highly reliable, and highly customizable network applications and tools that can be used to develop both servers and clients.

  • Why not use Java BIO?

    • One connection one thread, because the number of threads is limited, so this is very expensive resources, and ultimately it can not withstand the demands of high concurrent connections.
    • The CPU usage is low due to frequent context switching.
    • Poor reliability. All I/O operations are synchronized, even those of the service thread. Therefore, THE I/O operations of the service thread may be blocked.
  • Why not Use Java NIO?

    • NIO’s libraries and apis are quite complex, and to develop with it requires a very good command of Selector, ByteBuffer, ServerSocketChannel, SocketChannel, and so on.
    • Many additional programming skills are required to use NIO; for example, because NIO involves the Reactor threading model, you must be familiar with multithreading and network programming to write high-quality NIO programs.
    • To have high reliability, the workload and difficulty are very large, because the server needs to face frequent client access and disconnection, network intermittent disconnection, half packet read and write, failure cache, network congestion, these will seriously affect our reliability, and the use of native NIO to solve them is quite difficult.
    • JDK NIO in the famous BUG–epoll empty polling, when select returns 0, will cause Selector empty polling and cause cup100%, official said JDK1.6 after fixing this problem, in fact, just the probability of occurrence is reduced, there is no fundamental solution.
  • Why Netty?

    • API is simple to use, easier to use, low development threshold
    • Powerful, preset a variety of encoding and decoding functions, support a variety of mainstream protocols
    • With high customization capability, the communication framework can be flexibly expanded through ChannelHandler
    • High performance, Netty has the highest overall performance compared to many NIO mainstream frameworks
    • High stability, solve JDK NIO BUG
    • Experienced large-scale commercial application test, quality and reliability have been well verified.

From: Why develop with Netty

  • Why not use third-party SDK, such as Rongyun, Huanxin and Tencent TIM?

This is a matter of opinion, sometimes, because of the company’s technology selection, because the third-party SDK, means that the message data needs to be stored in the third-party server, moreover, the scalability, flexibility is certainly not better than their own development, there is a small problem, is the charge. For example, rongyun free version only supports 100 registered users, more than 100 will be charged, group chat support number is limited and so on…

Mina is actually a lot like Netty in that most of the API is the same because it was developed by the same author. However, it feels that Mina is not as mature as Netty, and it is easy to find a solution when problems arise in the process of using Netty, so Netty is a good choice.

All right, let’s cut to the chase.


The preparatory work

  • First, we will create a new Project. Inside the Project, we will create a new Android Library. The Module will be named im_lib.

  • Then, analyze our message structure. Each message should have a unique message ID, sender ID, receiver ID, message type, sending time, etc. After analysis, a general message type is sorted out as follows:

    • MsgId message id
    • FromId Indicates the id of the sender
    • ToId Id of the receiver
    • MsgType Message type
    • MsgContentType Message content type
    • Timestamp indicates the timestamp of a message
    • StatusReport statusReport
    • Extend field

    According to the above, I have compiled a mind map for your reference:

    This is the basic part, but you can customize your message structure to suit your needs.

    We write proto files based on our custom message types.Then execute the command (I use MAC, Windows command should be similar) :We will then see that a Java class is generated in the same directory as the proto file. This is what we need:Let’s take a peek:There are many things, don’t bother, this is the Protobuf class generated by Google for us, directly use it, how to use it? Just use this class file and copy it to the project package path we specified at the beginning:MessageProtobuf class file has no error. Netty jar package is also included. Fastjson:You are advised to use the netty-all-x.x.x. Terminal JAR package.

    Now that the preparation is over, let’s write Java code to implement instant messaging.


encapsulation

Why do you need encapsulation? In plain English, this is for decoupling, so that you can switch to a different framework implementation at a later date without having to modify the invocation everywhere. For example, in the early days of Android, the popular image loading framework was Universal ImageLoader. Later, due to some reasons, the original author stopped maintaining the project. Now the popular image loading framework is Picasso or Glide, because the image loading function can be called in many places. If you had used Universal ImageLoader earlier, without some encapsulation, and now had to switch to Glide, the momentum change would be very, very large, and there would be a very high risk of missing something.

So what’s the solution?

Quite simply, we can use the factory design pattern for some encapsulation. There are three kinds of factory patterns: simple Factory pattern, Abstract factory pattern, and factory method pattern. Here, I use the factory method mode to encapsulate. For specific differences, please refer to the following three design modes: Simple factory, factory method and abstract factory

The IMS (IM Service) existsInitialize the,Establish a connection,reconnection,Close the connection,Release resources,Check whether the long link is closed,Send a messageBased on the above analysis, we can carry out an interface abstraction: OnEventListener is a listener that interacts with the application layer:IMConnectStatusCallback is an IMS connection status callback listener:

Then write a Netty TCP implementation class:

Next, write a factory method:

That’s where the encapsulation ends, and the implementation comes in.


Initialize the

Let’s implementinit(Vector serverUrlList, OnEventListener Listener, IMSConnectStatusCallback callback), initialize some parameters, and make the first connection, etc:

Among them,MsgDispatcherIs a message forwarder that forwards received messages to the application layer:

ExecutorServiceFactoryIs a thread pool factory that schedules reconnection and heartbeat threads:


Connect and reconnect

resetConnectThe resetConnect() method is used as the starting point of the connection. The first connection and the reconnection logic are all handled in the resetConnect() method.As you can see, not for the first time to connect, that is, connect a period after the failure of reconnection, will let thread to sleep for a period of time, because this time may be network status is not very good, then, whether the ims are closed or for reconnection operation, because the reconnection operation is in the child thread execution, in order to avoid repeat reconnection, require some concurrent processing. After the reconnection task starts, perform the following steps:

  • Changes the reconnection status identifier
  • Callback connection state to the application layer
  • Close the previously open connection channel
  • Execute a new reconnection task using the thread pool

ResetConnectRunnableThe reconnection task, where the core reconnection logic is executed:

toServer() is where the server is actually connected:

initBootstrapInitialize Netty Bootstrap:Note: The number of NioEventLoopGroup threads is set to 4, which can meet the QPS of more than 1 million. If the application needs to withstand tens of millions of traffic, the number of threads needs to be adjusted. Since the reference:NioEventLoopGroup Specifies the number of threads

And then, let’s seeTCPChannelInitializerHanlder:Among them,ProtobufEncoderandProtobufDecoder– added support for ProtobufLoginAuthRespHandlerIs the handler that receives the server handshake authentication message response,HeartbeatRespHandlerIs the processing handler that receives the heartbeat message response from the server,TCPReadHandlerHandler is a handler that receives other messages from the serverLengthFieldPrependerandLengthFieldBasedFrameDecoder, this needs to be extended to TCP unpacking and sticky package.


TCP unpacking and sticky packet

  • What is TCP unpacking? Why does TCP unpack occur?

    In a nutshell, we all know that TCP is for data transmission in the form of “flow”, and TCP to improve performance, the sender will need to send data to brush into the buffer, wait for after the buffer is full, then the buffer data sent to the receiving party, by the same token, the receiver will have buffer mechanism, to receive data. Unpacking refers to reading only part of a packet without reading the whole packet during socket reading.

  • What is TCP sticky packet? Why does TCP sticky packet appear?

    Same as above. Sticky packet means that two or more packets are read and processed as one packet during socket reading.

Quoting a picture on the Internet to explain the three situations in TCP, such as unpacking, sticking and normal state, please contact me to delete:TCP unpacking/sticky packet causes, then, how to solve it? Generally speaking, there are four solutions:

  • Message fixed-length
  • End the message with a carriage return newline character
  • A special delimiter is used as the end of the message, such as \t, \n, etc. Carriage return line feed is actually one of the special delimiters.
  • A message is divided into a header and a body, and a field in the header identifies the total length of the message.

Netty encapsulates the following four decoders for the above four scenarios:

  • FixedLengthFrameDecoder fixed length message decoder
  • LineBasedFrameDecoder, carriage return newline message decoder
  • Decoder DelimiterBasedFrameDecoder, special delimiter news
  • LengthFieldBasedFrameDecoder, custom message length decoder.

We use is LengthFieldBasedFrameDecoder custom message length decoder, cooperate LengthFieldPrepender encoder to use at the same time, about the parameters configuration, suggestion of netty – the most common TCP sticky package solution: LengthFieldBasedFrameDecoder and LengthFieldPrepender, this article explain more carefully. We set the length of the message header to 2 bytes, so the maximum length of the message packet must be less than 65536 bytes. Netty stores the length of the message content in the field of the message header, and the receiver can get the total length of the message according to the field of the message header. Of course, Netty provides LengthFieldBasedFrameDecoder has sealed the processing logic, We only need to configure the lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip can, so that you can solve the unpacking and sticky package of TCP, This is where Netty comes in handy compared to native NIO, which takes care of unpacking/sticky packing itself.


Long-link handshake authentication

Next, let’s look at LoginAuthHandler and HeartbeatRespHandler:

  • LoginAuthRespHandlerIs when the client and the server after the success of the connection is established, the client take the initiative to send a login authentication message to the server, to the parameters associated with the current user, such as the token, the server receives the message, the user information to the database query, if it is legal and valid user, it returns a successful login message to the client, on the other hand, Return a login failure message to the client. In this case, it is the handler that receives the login status from the server, such as:

If status=1, the handshake is successful. In this case, a heartbeat message is sent to the server first. Then Netty’s IdleStateHandler reads and writes timeout mechanism is used. Periodically sends heartbeat messages to the server, maintains the long connection, and checks whether the long connection exists.

  • HeartbeatRespHandlerIs when the client receives the service side the login is successful, the initiative and a jump messages sent to the server, the heartbeat messages can be an empty bag, message inclusions as small as possible, the service side, after receipt of the client’s heartbeat package is returned to the client, here, the service side is received back to the processing of the heartbeat message response handler, such as:

This is relatively simple, received heartbeat message response, no task processing, directly print for our analysis.


Heartbeat mechanism and read/write timeout mechanism

Heartbeat packets are sent periodically, and you can also define a period, such as the Android wechat intelligent heartbeat scheme. For simplicity, it is specified here that a heartbeat packet should be sent every 8 seconds when the application is in the foreground, and every 30 seconds when the application is switched to the background. You can modify it according to your actual situation. Heartbeat packets are used to maintain long connections and check whether long connections are down.

We then use Netty’s read/write timeout mechanism to implement a heartbeat message management handler:As you can see, useuserEventTriggeredRead/write timeout/read/write timeout (IdleStateIdleStateHandlerCan be configured, the code will be posted below. The READER_IDLE event can be used to detect whether the server has not received a heartbeat packet response within a specified period of time, and if so, trigger a reconnection operation. The WRITER_IDEL event detects if the client has not sent a heartbeat packet to the server within a specified period of time, and if so, proactively sends a heartbeat packet. Sending heartbeat packets is performed in child threads, and we can use the previously written Work thread pool for thread management.

addHeartbeatHandler() code is as follows:As you can see from the picture, inIdleStateHandler, the read timeout is three times as long as the heartbeat interval. That is, if the heartbeat does not respond for three times, the long connection is considered disconnected and the reconnection is triggered. The write timeout is the heartbeatInterval, which means that a heartbeat packet is sent every heartbeatInterval. Read/write timeout is not used, so set to 0.

OnConnectStatusCallback (int connectStatus) is a connection status callback and some common logic handling:Once the connection is successful, send a handshake message to review the whole process again:

  • The client makes the first connection according to the host and port returned by the server.
  • After a successful connection, the client sends a handshake authentication message to the server (1001)
  • After receiving the handshake authentication message from the client, the server obtains the user token from the extended field and checks the validity of the token from the local database.
  • After the verification is complete, the server sends the verification result back to the client in a 1001 message, that is, a handshake message.
  • After receiving the handshake message from the server, the client retrieves the verification result from the extended field. If the check is successful, the client to the server sends a heart jump message (1002), and then into the heart to send cycle, regular interval heartbeat messages sent to the server, maintain a long connection link availability and real-time detection, if discover the link is not available, waiting for a period of time to trigger reconnection operation, after the success of the reconnection, start shaking hands/heartbeat logic.

Take a look atTCPReadHandlerHow to deal with the message received: As you can see, inchannelInactive() andexceptionCaught() methods all trigger reconnection,channelInactiveThe () method is called when the link is down,exceptionCaughtThe () method fires when an exception occurs, and so onchannelUnregistered(),channelReadComplete() and other methods can be rewritten, here will not be posted, I believe that you can see the role of smart method at a glance.

The channelRead() method is used to determine if the message was successfully sent. If the message was successfully sent, it is removed from the timeout manager. We’ll talk more about the message retransmission mechanism next. Else, after receiving other messages, it immediately returns a message reception status report to the server, telling the server that I received this message. This action will be useful for subsequent offline messages that need to be made. If you do not need to support offline messaging, skip this step. Finally, a message forwarder is invoked to forward the received messages to the application layer.

After writing so much code, let’s take a look at the effect after running, first paste the missing message sending code and IMS shutdown code and some default configuration items of the code.

Send a message:Close the ims:Default IMS configuration:Also, the IMS Client initiator implemented by the application layer:Since there are a lot of codes, it is not convenient to post all of them. If you are interested, you can download the demo experience. Oh, and there’s a simple server-side code that looks like this:


debugging

Let’s take a look at the connecting and reconnecting part first (since recording giFs is cumbersome and bulky, I first set the reconnecting interval to 3 seconds to see the effect).

  • Start the server:
  • Start the client:

If the server is not started, the client will be reconnected. If the server is not started, the client will be reconnected.This time we started the client first, you can see that the connection has been reconnected after the failure, because it is difficult to record GIF, after the third failed connection, I started the server, at this time the client will be successfully reconnected.

Then, let’s debug the handshake authentication message, which is the heartbeat message:After the long connection is successfully established, the client sends a handshake authentication message (1001) to the server. After receiving the handshake authentication message, the server returns a handshake authentication status message to the client. After receiving the handshake authentication status message, the client starts the heartbeat mechanism. Gifs are not easy to demonstrate, so download the demo to see them intuitively.

Next, after talking about message retransmission mechanism and offline message, I will do some simple encapsulation in the application layer and run on the simulator, so that you can see the operation effect intuitively.


Message retransmission mechanism

Message resending, as the name implies, even if a message fails to be sent. Considering the instability and variability of the network environment (such as entering the elevator, subway, mobile network switching to wifi, etc.), the probability of message sending failure is not small. In this case, the message retransmission mechanism is necessary.

Let’s start by looking at the code logic implemented. MsgTimeoutTimer: MsgTimeoutTimerManager:

Then, let’s look at the transformation of TCPReadHandler to receive messages:Finally, take a look at the transformation of sending messages:

Here’s the logic: When sending a message, in addition to the heartbeat messages, shaking hands, status reports, news, message sending messages to join the timeout manager, immediately start a timer, such as once every 5 seconds, were performed three times, in this period, if the message not sent successfully, will resend three times, three times after resend if there is still no sent successfully, then give up retransmission, After the message is removed, the application layer is notified through the message forwarder, and the application layer decides whether to resend the message. If the message is successfully sent, the server returns a message sending status report. After receiving the status report, the client removes the message from the message sending timeout manager and stops the timer corresponding to the message.

In addition, when the user succeeds in handshake authentication, it should check whether there are timeout messages in the message sending timeout manager, and resend all messages if there are:


Offline message

Because of the offline message mechanism, the server database and cache need to cooperate, the code is not posted, too much too much, I briefly say the implementation idea: client A sends A message to client B, the message will first to the server, by the server for transfer. At this point, client B has two cases:

  • 1. The long connection is normal, that is, the client network environment is good, the phone is powered on, and the application is open.
  • 2. Duh, that must be the long connection is abnormal. This can happen for a variety of reasons, such as wifi not working, users in bad places such as the subway or elevator, apps not open or logged out, and in general, not being able to receive messages properly.

If it is a long connection, there is nothing to say, the server can directly forward. If the long connection is abnormal, the server responds to the message sent from client A to client B. After receiving the message, the server sends A status report to inform client A that the message has been received. At this time, client A does not care about the message as long as the message reaches the server. Then, the service side try to forward the message to client B, if at this time the client receive the service side B forwarded message, need to a status report, the service side back immediately tell the service side, I have received the message, the server in the news from your client B returned after receiving a status report that the message has been normal to send, no longer need to inventory. B if the client is not online, when doing the forwarding service end, did not receive news from your client B returned a status report, so, this news should save to the database, until the client B after launch, which is long after the success of the connection is established, the client B take the initiative to ask the server to send an offline message, the server after I receive my offline message asking, To the database or cache to check client B all the offline message, and return in batches, client B after receiving server offline message returns, take out the message id (if there is more than just take id set), through offline message response message id is returned to the server, the server after receive, according to the message id from the database to delete the corresponding message. The above is the case of single chat offline message processing, group chat is a little different, in the case of group chat, the server needs to confirm that all users in the group have received the message before deleting the message from the database, so much for that, if you need details, you can send me a private message.


NettyTcpClient defines a number of variables, in case you do not understand the definition of variables, but also posted code:

Application layer encapsulation

This is a matter of opinion, everyone’s code style is different, I put my own simple encapsulation of the code posted:

MessageProcessor: IMSEventListener Listener for interacting with the IMS: MessageBuilder: AbstractMessageHandler AbstractMessageHandler, each message type corresponds to a different messageHandler:SingleChatMessageHandler SingleChatMessageHandlerGroupChatMessageHandler GroupChatMessageHandler:MessageHandlerFactory specifies the MessageHandlerFactory:MessageType Enumeration of message types:IMSConnectStatusListenerIMS connection status listener:Because each person’s code style is different, the package code has its own idea, so, here is not more than explain, just put their own simple package code all posted up, as a reference. Just know that the dispatchMsg(MessageProtobuf.Msg Msg) method of OnEventListener is called when a message is received:To send a message, call imsClient’s sendMsg(MessageProtobuf.Msg Msg) method:Can, as to how to package better, everyone free play.


Finally, to test whether messages are sending and receiving properly, we need to change the server: As can be seen, when a user successfully shakes hands, the corresponding channel of the user is saved to the container. When sending messages to the user, the corresponding channel is extracted from the container according to the user ID and used to send messages. When a user disconnects, the user’s channel is removed from the container.

Run it and see what it looks like:

  • First, start the server.
  • Then, change the IP address of the client connection to 192.168.0.105 (which is my local IP address), port number to 8855, fromId (userId) defined to 100001, toId to 100002, and start client A.
  • Then fromId, or userId, defined as 100002 and toId as 100001, starts client B.
  • Client A sends A message to client B. You can see that client B has received the message.
  • If client B sends A message to client A, it can also be seen that client A has received the message.

As for the message sending test, it was successful. As for group chat or reconnection functions, I will not demonstrate one by one, or the same words, download the demo to experience it…

Due to the large size of GIF recording, so can simply demonstrate the message, specific download demo experience it…

If there is a need for application-level UI implementation (that is, encapsulation of chat pages and session pages), I will share it.

Making the address




Found a bug

  1. MsgTimeoutTimer:

This bug is found when I check the code, may be staying up for a few days to write an article… Modified as follows:

A person’s energy is limited, we use in the process, if you find other bugs, please tell me, anyway, I will humbly accept, resolutely not to change, bah, change, change. In addition, welcome to Fork, I look forward to working with you to improve…




Write in the last

Finally finished, this article about 10 days to write, a large part of the reason is that I have procrastination, each time after writing a short paragraph, always can not calm down to write, so it has been delayed until now, I have to change later. The first time to write technology to share the article, there are a lot of places maybe logic is not too clear, due to the space is limited, but also posted part of the code, I suggest you download the source code down to see. Always wanted to write this article, has also tried to find on the Internet a lot before im articles, all can not find a more perfect, this article not perfect, but contains a lot of module, hope to have the effect of a topic, looking forward to you together with me also find more problems and perfect, in the end, if this article is useful to you, Hope to give me a star on Github…

The netty-all-4.1.33.final.jar package was streamlined as requested. The original netty-all-4.1.33.final. jar package is 3.9m, and the im_lib library only needs the following JAR package:

  • Netty – buffer – 4.1.33. Final. The jar
  • Netty – codec – 4.1.33. Final. The jar
  • Netty – common – 4.1.33. Final. The jar
  • Netty – handler – 4.1.33. Final. The jar
  • Netty – resolver – 4.1.33. Final. The jar
  • Netty – transport – 4.1.33. Final. The jar

Therefore, extract the above JAR package and re-type it into NetTY-TCP-4.1.33-1.0.jar. There is no problem in the current self-test. If you find any bug, please tell me, thank you.

The size comparison between the original JAR and the clipped JAR package is attached: The code has been updated to Github.


Next, I will take time to write all the articles I want to write below, in no order, and write them wherever I think of.

In addition, I created an Android INSTANT messaging technology communication QQ group: 1015178804, students who need to add, I will try to answer the questions that do not understand, learn together, grow together.

The end.

PS: The newly opened public account can not leave a message, if you have different opinions or suggestions, you can go to the nuggets comment or add to the QQ group: 1015178804, if the group is full, you can also give me a private message on the public account, thank you.

Post the official number:

FreddyChen