Open-source project
Lihuanghe/SMSGate avatar
Lihuanghe/SMSGate

SMSGate: a Netty 4 framework for China's three carrier SMS protocols

这是一个在netty4框架下实现的三网合一短信网关核心框架,支持(cmpp/smpp3.4/sgip1.2/smgp3) 短信协议解析,支持长短信合并和拆分,也支持wap短信和闪信。

1,190 stars513 forksJavaApache-2.0

At a glance

What is it?
SMSGate is an Apache-2.0 Java framework that speaks the CMPP, SMPP 3.4, SGIP 1.2 and SMGP 3 carrier messaging protocols over Netty 4, handling long message splitting and merging, flash messages, proxies and delivery report correlation.
Who is it for?
SMSGate suits a Chinese team that has to speak one or more carrier SMS protocols directly and wants long message handling, delivery report correlation and connection management without writing the protocol layer, and who is prepared to work in Netty. It does not fit a team that only needs to send SMS through an aggregator API, and it does not fit a project that cannot carry a Netty-based server.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 113 days ago.
What is it written in?
Mainly Java, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Four carrier protocols behind one API

SMSGate is a Java framework that implements a combined gateway for the three Chinese carriers, built on the Netty 4 networking framework. It parses four protocols: CMPP, SMPP 3.4, SGIP 1.2 and SMGP 3.

Those four cover the main dialects a Chinese SMS gateway has to speak. The point of supporting them in one codebase is that the wire formats differ in their headers, their login handshakes and their error codes, but the business shape is the same, so a gateway that speaks all four can route to whichever carrier channel a customer is on.

The repository is a Maven project with `src/`, `doc/` and a `pom.xml`, and the artefact is published under the group `com.chinamobile.cmos`:

xml
<dependency>
  <groupId>com.chinamobile.cmos</groupId>
  <artifactId>sms-core</artifactId>
  <version>2.1.13.6</version>
</dependency>

The licence is Apache-2.0, and the default branch is `netty4`, which is a leftover of the port and a useful signal about the project's history. The README's first line is a request to add the technical questions to a QQ group, and the group is named for the CMPP side of the project specifically.

The newest release tag is v2.1.13.6 from 2024-09-26, and the previous one, v2.1.13.5, dates from 2023-05-12. The last push was on 2026-07-04, so work continues on the branch without new tags.

The connection model, and why it confuses newcomers

The FAQ answers the question that trips up everyone new to this codebase, which is how you actually send a message.

The explanation is that the SMS protocol is a TCP long connection, similar to a database connection, so you must have a connection before sending. You establish it at application start, by calling the manager's `openEntity()` method, and you call `manager.startConnectionCheckTask()` to start the reconnect handling. After that you build a message object and send it with `ChannelUtil.syncWriteLongMsgToEntity`, passing the client entity id and the message.

The return value is a `List` of futures, and the reason is worth internalising: one logical message can be split into several SMS parts, so a single send can produce several results. The list being null means the send failed.

That framing also explains the shape of the rest of the API. The framework is not a facade with a `send(to, body)` method; it is a set of protocol-aware entities, connectors and session managers that you extend when you need to change behaviour.

The README's own suggestion for a first run is concrete: import the project, run the `TestCMPPEndPoint` demo, and learn the credential parameters from there. It also says you need some Netty basics first, since the code is built on that framework.

Overspeed errors, retries and the per-protocol difference

One protocol behaviour is worth understanding before you let this framework talk to a carrier in production, because it involves automatic resending.

Under CMPP, if the gateway receives a response with error code 8, which is the overspeed error, the system resends by default until it succeeds, with a maximum retry count of 30. To turn that off you set `entity.overSpeedSendCountLimit` to 0.

SGIP and SMPP use 88 as their overspeed error code and behave the same way, resending on it. SMGP is the exception, because that protocol does not define an overspeed error code, so there is no overspeed retry there at all.

That asymmetry is a good illustration of what this framework actually is. It is not a neutral abstraction over four similar protocols, it is a place where the differences between them are visible and have to be reasoned about. A default of up to 30 automatic resends on a specific error is also a decision you should consciously accept rather than inherit, since repeated sends against a carrier that is throttling you is exactly the situation where you want to stop and look.

Long messages, by contrast, are already handled. The framework splits a long message into short segments automatically, generally at 67 Chinese characters, and merges incoming segments, so sending one is no different from sending a normal message.

Flash messages, proxies and reading the wire log

Three smaller features are documented in enough detail to be useful.

Flash messages, which are a Chinese carrier service for text that appears directly on the recipient's screen, are sent through a normal message object with a different message class. The sample creates a submit request and sets the content with `SmsAlphabet.UCS2` and `SmsMsgClass.CLASS_0`, where class 0 is what makes it a flash message:

java
CmppSubmitRequestMessage msg = CmppSubmitRequestMessage.create(phone, "10690021", "");
msg.setMsgContent(new SmsTextMessage("你好,我是闪信!",SmsAlphabet.UCS2,SmsMsgClass.CLASS_0));

Proxy support covers HTTP, HTTPS and SOCKS, which matters when the carrier channel is reached through a proxy:

java
client.setProxy("http://username:password@ipaddress:port");
client.setProxy("socks5://username:password@ipaddress:port");

And for debugging a protocol problem, the framework logs traffic per entity, with the logger name taken from the entity id. Debug level prints the message object's `toString`, and trace level prints the binary content. The logger name follows the pattern `entity.` plus the entity id, for example `entity.cmppclientEntityId`, which is how you target a single channel in a logback or log4j2 configuration.

For running on Java 9 and later, the process needs several `--add-opens` and `--add-exports` options covering `java.base/java.lang`, `java.base/java.math`, `java.base/java.util`, `java.base/java.util.concurrent`, `java.base/java.net` and `java.base/java.text`, plus an export of `java.base/sun.security.x509`. This is the standard cost of a framework that reflects into the JDK internals, and it needs to be in the start-up script rather than in the build.

Long message merging across a cluster, and the SPI hook

The most interesting design decision in the framework concerns long message merging in a multi-node deployment.

Long message merging on the phone side works like IP fragmentation. The first six bytes of the message PDU are the long message header, the first three fixed at `0x050003`, and the last three carry the merge basis: a packet id of up to 255, a total fragment count, and a fragment index. So 45, 03, 01 means fragment 1 of 3 for packet id 45. The handset reassembles them and shows one message, while the recipient is charged for each fragment received. A gateway therefore has to merge on receipt and split on send.

The framework ships a JVM in-memory cache, the `LongMessageFrameCache` class built on a Guava Cache, to hold incomplete fragments. The problem appears in a clustered deployment, where different fragments of the same long message can arrive on different nodes, and a per-JVM cache cannot combine them.

The solution is an SPI mechanism. The framework loads `LongMessageFrameCache` implementations through SPI, and the business side supplies a Redis-backed provider, `RedisLongMessageFrameProvider`. The rule is that your implementation's `order()` must be greater than 0, and the framework prefers the implementation with the highest order. A reference implementation, `RedisLongMessageFrameCache` and `RedisLongMessageFrameProvider`, exists in the test package.

There is a performance caveat attached: you have to enable the `EndpointEntity.isRecvLongMsgOnMultiLink` property on the channel account, because only a few systems need it and leaving it on for every account affects merge performance.

Delivery reports, UniqueLongMsgId and the proxy protocol

Correlating what you sent with what the carrier reports back is where a gateway earns its complexity, and the README covers the mechanics in detail.

The basic link is the `msgId`. When the carrier responds to a submit request you receive a `submitResponse` containing a `msgId`, and the later status report carries the same `msgId`, so the two can be matched. When you act as a gateway in front of your own customers, the arithmetic gets more involved, because there are two msgIds: the one the channel gives you and the one you gave your customer. The README says to record at least the source customer, the outbound channel, both msgIds and the message body, then update the status through the channel msgId and pass the report back to the customer using the msgId you used when replying to them.

The `UniqueLongMsgId` exists to solve a specific problem in that flow. Because protocols like CMPP and SGIP are asynchronous, and because long messages are split and merged internally, the message received from the service provider and the message sent to the operator have no shared identifier, so a returning status report is hard to match. `UniqueLongMsgId` provides that link. It is set when the message arrives from the network before merging, does not change until it is forwarded to the operator channel, and is the same across all fragments of one long message. It is also unique even when the same phone number and port send in quick succession.

Separately, for gateways sitting behind nginx or haproxy, the framework can read the real client IP using the proxy protocol. You enable it with `setProxyProtocol(true)` on a `ServerEndpoint`, and the framework takes the real address from the first message on the channel, an `HAProxyMessage`, storing it in a channel attribute readable as `channel.attr(GlobalConstance.proxyProtocolKey)`.

Editorial conclusion

SMSGate suits a Chinese team that has to speak one or more carrier SMS protocols directly and wants long message handling, delivery report correlation and connection management without writing the protocol layer, and who is prepared to work in Netty. It does not fit a team that only needs to send SMS through an aggregator API, and it does not fit a project that cannot carry a Netty-based server. Before adopting it, read the CMPP interface protocol document in the doc directory, run the TestCMPPEndPoint demo to configure credentials, and check the Java 9 and later start-up flags, since the framework needs add-opens and add-exports options to run on a modern JVM.

Frequently asked questions

Which SMS protocols does SMSGate support?

It parses CMPP, SMPP 3.4, SGIP 1.2 and SMGP 3, and it also handles WAP messages and flash messages. The framework runs on Netty 4 and is published to Maven as com.chinamobile.cmos:sms-core.

How do I send a message with SMSGate?

You must open a connection first, since the protocol is a TCP long connection. Call the manager's openEntity() method at start-up and startConnectionCheckTask() for reconnection, then build a message and send it with ChannelUtil.syncWriteLongMsgToEntity, which returns a List of futures because one message can be split into several parts.

How does SMSGate handle long messages?

Long messages are split automatically into short segments, generally at 67 Chinese characters, and incoming segments are merged using the six byte PDU header where the first three bytes are fixed at 0x050003 and the last three carry the packet id, total fragment count and fragment index.

How do I stop SMSGate from resending on overspeed errors?

Under CMPP an error code of 8 is an overspeed error that triggers automatic resending, with a default limit of 30 retries, and SGIP and SMPP use code 88 for the same behaviour. Set entity.overSpeedSendCountLimit to 0 to disable it.

How do I merge long messages across a cluster of gateway nodes?

The framework loads LongMessageFrameCache implementations through SPI, so you can supply a Redis-backed provider such as RedisLongMessageFrameProvider. Its order() must be greater than 0 and the framework prefers the highest order, and you enable the channel account property isRecvLongMsgOnMultiLink.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. Lihuanghe/SMSGate on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lihuanghe-smsgate.svg)](https://hysenlabs.com/projects/lihuanghe-smsgate)

Was this useful?