preface
In the past, many push messages were constantly sent to the server at a certain interval through the front end. However, such a disadvantage is that it wastes a lot of server resources and may be abused, leading to server abnormalities. Hence the emergence of the Websocket protocol. The advantages of WebSocket protocol are that it can realize persistent connection, save server resources and bandwidth, and communicate in more real time. This article is mainly about the use of Springboot + Websocket back to the front end push message function.
resources
- One Linux server (for project deployment)
- Intellij IDEA (for back-end coding software)
- Android Studio (Android Coding Software)
Purpose and Effect
Realize the backend of the server to send different types of notification to different channels, different user groups (such as status notification, start pop-ups, etc.). Group push: all online devices will receive push push: only the input device ID number can receive push push channel: push conditional push for a certain market channel: push combined with multiple conditions, such as user age range, market channel and other highly targeted push
The backend implementation
Create projects using Indellij idea
Step one:
Step 2:
Setting.xml reads as follows:
<? xml version="1.0" encoding="UTF-8"? > <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <mirrors> <! -- mirror | Specifies a repository mirror site to use instead of a given repository. The repository that | this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used |for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>uk</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://uk.maven.org/maven2/</url>
</mirror>
<mirror>
<id>CN</id>
<name>OSChina Central</name>
<url>http://maven.oschina.net/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>nexus</id>
<name>internal nexus repository</name>
<url>http://repo.maven.apache.org/maven2</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>
Copy the code
After the configuration change, it is easy to connect. The following image is a directory structure for the project
Config: websocket config class: websocket config class: websocket config class: websocket config class: websocket config class: websocket config class: websocket config class: websocket config class: websocket config class: websocket config class: websocket config class: websocket config class Store static files, such as CSS and JS templates: Store HTML templates for display in the user’s front-end interface
Step 3:
To configure the dependencies, here is my poem. XML file
<? xml version="1.0" encoding="UTF-8"? > <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> < modelVersion > 4.0.0 < / modelVersion > < the parent > < groupId > org. Springframework. Boot < / groupId > The < artifactId > spring - the boot - starter - parent < / artifactId > < version > 2.2.1. RELEASE < / version > < relativePath / > <! -- lookup parent from repository --> </parent> <groupId>com.push</groupId> <artifactId>push</artifactId> < version > 0.0.1 - the SNAPSHOT < / version > < name > push < / name > < description > Demo projectforSpring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <! -- This needs to betrueHot deployment only works --> <optional>true</optional> </dependency> <! Javax. servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <! -- We need Toncat. --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope> </dependency> <! <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <! You can use the templates folder in your resource library, and put the static content in your Resource folder. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <! -- Gson is easier to use, json processing is more convenient, Gson </groupId> <artifactId>gson</artifactId> <version>2.8.6</version> </dependency> </dependencies> <build> <plugins> <! <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>Copy the code
The more important dependencies are annotated
Step 4
Write a configuration class of controller and Websocket, controller is mainly used to intercept various connections and make corresponding processing, Websockt configuration class in the project is mainly to intercept the Websocket address. The websocket code is as follows:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
returnnew ServerEndpointExporter(); } @ Override public void registerWebSocketHandlers (WebSocketHandlerRegistry registry) {/ / register the user here, To intercept the connection registry. AddHandler (new UserHandler(),"/user")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*"); Registry. addHandler(new AdminHandler(),"/admin")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*"); }}Copy the code
The code for controller is as follows:
@controller public class PushMainEnterController {/** * group message * @param messageBean records the content sent from the background management page * @return
*/
@ResponseBody()
@RequestMapping(value = "/sendAll",method = RequestMethod.POST , produces = "application/json; charset=UTF-8")
public String sendAll(@RequestBody MessageBean messageBean){
Gson gson = new Gson();
if(messageBean ! = null){ String backInfo = gson.toJson(messageBean); / / send a message the front-end equipment WebSocketUtils. SendMessageToUser (backInfo); }else{/ / administrator message sent failure WebSocketUtils. SendMessageToAdmin ("Send failed");
}
return "{}"; } // The code for any other push, conditional push, channel push and group message is basically the same, but the method called in WebSocketUtils is different. /** * The connection defaults to starter. This page is the main interface of the management background * @param mv * @return
*/
@ResponseBody
@RequestMapping(value="/")
public ModelAndView index(ModelAndView mv){
mv.setViewName("starter");
returnmv; }}Copy the code
WebSocketUtils auxiliary class preparation, mainly convenient to send messages, add users and other operations. I originally wanted to save it using hashMap, so I could get the content directly from the key, but since HashMap is thread unsafe and could have problems with multithreading, I decided to use CopyOnWriteArraySet instead.
Public class WebSocketUtils {private static CopyOnWriteArraySet<WebSocketSession> usersSessionSet = new CopyOnWriteArraySet<>(); Private static CopyOnWriteArraySet<WebSocketSession> adminSessionSet = new CopyOnWriteArraySet<>(); Public static synchronized void addAdmin(WebSocketSession socketSession){ adminSessionSet.add(socketSession); } public static synchronized void addUser(WebSocketSession socketSession){ usersSessionSet.add(socketSession); } @param webSocketSession public static synchronized void removeSession (webSocketSession webSocketSession){ adminSessionSet.remove(webSocketSession); } @param webSocketSession public static synchronized void removeSession (webSocketSession webSocketSession){ usersSessionSet.remove(webSocketSession); } /** * Obtain the number of online administrators * @return
*/
public static synchronized int getAdminOnlineCount() {returnadminSessionSet.size(); } /** * Obtain the number of online users * @return
*/
public static synchronized int getUserOnlineCount() {returnusersSessionSet.size(); } /** * Send messages to the administrator */ public static voidsendMessageToAdmin(){
adminSessionSet.forEach(webSocketSession -> {
try {
webSocketSession.sendMessage(new TextMessage("Number of people online is:"+getUserOnlineCount()));
} catch (IOException e) {
e.printStackTrace();
System.out.println("Failed to send message to administrator:"+e.getLocalizedMessage()); }}); } public static void sendMessageToAdmin(String MSG){adminSessionSet. ForEach (webSocketSession -> {try { webSocketSession.sendMessage(new TextMessage(msg)); } catch (IOException e) { e.printStackTrace(); System.out.println("Failed to send message to administrator:"+e.getLocalizedMessage()); }}); } /** * public static void sendMessageToUser(String MSG){ usersSessionSet.forEach(usersSessionSet->{ try { usersSessionSet.sendMessage(new TextMessage(msg)); } catch (IOException e) { e.printStackTrace(); System.out.println("Message sending failed"); }}); } / * * * is sent to a user * / public static void sendMessageToUserForSingle (String MSG) {Gson Gson = new Gson (); MessageBean messageBean = gson.fromJson(msg,MessageBean.class); usersSessionSet.forEach(webSocketSession -> { String[] path = webSocketSession.getUri().getQuery().split("&");
HashMap<String,String> map = new HashMap<>();
for(int i=0; i<path.length; i++){ String[] para= path[i].split("=");
map.put(para[0],para[1]);
}
if (map.get("id").equals(messageBean.getTargetId())){
try {
webSocketSession.sendMessage(new TextMessage(msg));
} catch (IOException e) {
e.printStackTrace();
System.out.println(Failed to send message to user:+e.getLocalizedMessage()); }}}); } / sent to a channel of * * * * / public static void sendMessageToUserForChannel (String MSG) {Gson Gson = new Gson (); MessageBean messageBean = gson.fromJson(msg,MessageBean.class); usersSessionSet.forEach(webSocketSession -> { String[] path = webSocketSession.getUri().getQuery().split("&");
HashMap<String,String> map = new HashMap<>();
for(int i=0; i<path.length; i++){ String[] para= path[i].split("=");
map.put(para[0],para[1]);
}
if (Integer.valueOf(map.get("channel")) == messageBean.getChannel()){
try {
webSocketSession.sendMessage(new TextMessage(msg));
} catch (IOException e) {
e.printStackTrace();
System.out.println(Failed to send message to user:+e.getLocalizedMessage()); }}}); } / sent via condition information * * * * / public static void sendMessageToUserForCondition (String MSG) {Gson Gson = new Gson (); MessageBean messageBean = gson.fromJson(msg,MessageBean.class); ForEach (webSocketSession -> {usersSessionSet. ForEach (webSocketSession -> { String[] path = webSocketsession.geturi ().getQuery().split()"&");
HashMap<String,String> map = new HashMap<>();
for(int i=0; i<path.length; i++){ String[] para= path[i].split("="); map.put(para[0],para[1]); } // Determine whether the channel meets the condition parameters, and send a message if soif (Integer.valueOf(map.get("channel")) == messageBean.getChannel() && Integer.valueOf(map.get("age"))>messageBean.getMinYear() && Integer.valueOf(map.get("age"))<messageBean.getMaxYear()){
try {
webSocketSession.sendMessage(new TextMessage(msg));
} catch (IOException e) {
e.printStackTrace();
System.out.println(Failed to send message to user:+e.getLocalizedMessage()); }}}); }}Copy the code
Construct a MessageBean, this class is mainly used to accept front-end management background sent parameters, such as push title, push content, push channel, etc.
Public class MessageBean {private String title; // Push title private String content; Private String imageUrl; // Push image private inttype; // Push type private String targetId; // Push target id private int minYear; Private int maxYear; Private int channel = -1; private int channel = -1; // push channel}Copy the code
Define two Handles that handle user and administrator connections. When the user is successfully connected or disconnected, it pushes a message to the administrator so that the administrator can obtain the online user. Here, only the handle of the user is displayed
Public class UserHandler implements WebSocketHandler {/** * This method is called when the connection succeeds */ @override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String para = session.getUri().getQuery(); WebSocketUtils.addUser(session); . / / report to the administrator of the current online WebSocketUtils sendMessageToAdmin (); } @override public void handleMessage(WebSocketSession session, WebSocketMessage<? > message) throws Exception {} /** * Call this method when the connection is abnormal */ @override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { System.out.println("User connection failed"); } @override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { System.out.println("User disconnects"); WebSocketUtils.removeUser(session); . / / report to the administrator of the current number of WebSocketUtils sendMessageToAdmin (); } @Override public booleansupportsPartialMessages() {
return false; }}Copy the code
The code for adminHandle is the same as the code for UserHandle. Of course, adminHanle is not necessary if you do not need to display dynamic statistics in the admin interface.
Package the code into JAR format and deploy it to the server
packaging
When I first started packing the jar with Intellij idea, I ended up printing the JAR, but the jar didn’t actually run, and it would tell me something like “missing manifest file”, basically missing the meta-INF file. MVN clean and MVN install can be used to pack the package, so I used MVN clean and MVN install to pack the package. The result was not successful, there was an error: Failed to execute goal org, apache maven. Plugins: maven – surefire plugin: 2.22.2: test (default – test) on the project push: There are test failures. Feeling no solution and then looked for other commands, and finally saw a great god online said that this situation can be packaged with another command. Skip =true MVN clean package -dmaven.test. skip=true
Upload the package
Here we use the SCP command to upload the JAR package to the server (my programming environment is Linux) using the following command: SCP file name Username@server IP address: specifies the server target folder, for example, SCP pushadmin. jar [email protected]:/home
Start the server
Use SSH to connect to the server. For example, SSH [email protected], connect to nohup java-jar, and use nohup java-jar. Jar & to start and suspend the backend. Here the deployment is complete, then the external network can normally access the background management.
Implementation of Android code
Android connection to websocket we use okHttp3 to implement, since the use of okHttp3, must introduce a dependency, dependency is as follows
implementation 'com. Squareup. Okhttp3: okhttp: 3.8.1'
implementation 'com. Squareup. Okhttp3: mockwebserver: 3.8.1'
Copy the code
The following code posts only the connection part of the code, other non-core content is not posted.
// The client is a common user, so use the user request: channel: device ID: age: age. Val request = request.builder ().url(val request = request.builder ().url("ws://xxx.xxx.xxx.xxx:8080/user? channel=0&id=0&age=22").build() val socketListener = WebSocketCallback() var mOkHttpClient = okHttpClient.builder () Timeununit.seconds) // set the writeTimeout time.writetimeout (3, timeununit.seconds) // set the connection timeout time.connecttimeout (3, TimeUnit.SECONDS) .build() mOkHttpClient!! .newWebSocket(request, object:WebSocketCallbackOverride fun onOpen(webSocket: webSocket? , response: Response?) { super.onOpen(webSocket, response) text!! .setText("Connection Status: Connection successful"Override fun onMessage(webSocket: webSocket? , text: String?) { super.onMessage(webSocket, text) content!! .settext (text) // We can start a popover for notification. The data sent from the back end should be json, which is easy to parse. , code: Int, reason: String?) { super.onClosed(webSocket, code, reason) text!! .setText("Connection status: failed to connect"} // Override fun onClosing(webSocket: webSocket? , code: Int, reason: String?) { super.onClosing(webSocket, code, reason) Log.e("Log"."Link closed"Override fun onFailure(webSocket: webSocket? , t: Throwable? , response: Response?) {super. OnFailure (webSocket, t, response)}}) / / / / close the connection service mOkHttpClient. The dispatcher (). The executorService () shutdown ()Copy the code
The effect is the GIF image at the beginning of the article. Of course, I ignore the front-end management and background construction here, because this is not the focus of the article, so I ignore it. I’m actually using AdminLTE-3.0.0 for the front end management background, let the back end write some JS and exchange data with the back end.
conclusion
Advantages:
- It has strong scalability. If we need any push conditions, we just need to complete them. If we use third-party push such as Umeng Push, we will often find that the push conditions they provide to us are not exactly what we need.
- Push targets accurately. The clearer the target, the easier it is to achieve the promotion of their products
- Convenient access to distribution information. Because there are parameters passed when connecting to websocket, if there are enough of these parameters, it is easy to know what the user composition is
- To save money, umeng and other third parties cannot avoid charging. If it is its own push background, it can save this cost. Meanwhile, some key information is also in its own hands.
disadvantages
- Firstly, the push system needs to be developed and maintained. Secondly, it can be used by direct access of a third party.
- Second: when too many online users may affect the performance of the server, after all, each online user is an entity, if there are tens of millions of online users, it means that the server must create thousands of entities, the server can not eat. I haven’t stress-tested it yet, and it’s hard to know what the impact will be on the server.
- Third: push may not accurately hit the target. For a target device to receive a push, the device must be online. There are a lot of ways to keep your process alive on Android phones, but it can’t be 100% effective unless you’re a unicorn like Alibaba or Tencent that can be whitelisted by mobile systems.