集成 websocket 的四种方案

1. 原生注解

pom.xml

org.springframework.boot

spring-boot-starter-websocket

WebSocketConfig

/*

* *

* * blog.coder4j.cn

* * Copyright (C) 2016-2019 All Rights Reserved.

*

*/

package cn.coder4j.study.example.websocket.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.socket.config.annotation.EnableWebSocket;

import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**

* @author buhao

* @version WebSocketConfig.java, v 0.1 2019-10-18 15:45 buhao

*/

@Configuration

@EnableWebSocket

public class WebSocketConfig {

@Bean

public ServerEndpointExporter serverEndpoint() {

return new ServerEndpointExporter();

}

}

说明:

这个配置类很简单,通过这个配置 spring boot 才能去扫描后面的关于 websocket 的注解

WsServerEndpoint

/*

* *

* * blog.coder4j.cn

* * Copyright (C) 2016-2019 All Rights Reserved.

*

*/

package cn.coder4j.study.example.websocket.ws;

import org.springframework.stereotype.Component;

import javax.websocket.OnClose;

import javax.websocket.OnMessage;

import javax.websocket.OnOpen;

import javax.websocket.Session;

import javax.websocket.server.ServerEndpoint;

import java.io.IOException;

import java.util.HashMap;

import java.util.Map;

/**

* @author buhao

* @version WsServerEndpoint.java, v 0.1 2019-10-18 16:06 buhao

*/

@ServerEndpoint("/myWs")

@Component

public class WsServerEndpoint {

/**

* 连接成功

*

* @param session

*/

@OnOpen

public void onOpen(Session session) {

System.out.println("连接成功");

}

/**

* 连接关闭

*

* @param session

*/

@OnClose

public void onClose(Session session) {

System.out.println("连接关闭");

}

/**

* 接收到消息

*

* @param text

*/

@OnMessage

public String onMsg(String text) throws IOException {

return "servet 发送:" + text;

}

}

说明

这里有几个注解需要注意一下,首先是他们的包都在 **javax.websocket **下。并不是 spring 提供的,而 jdk 自带的,下面是他们的具体作用。

@ServerEndpoint

通过这个 spring boot 就可以知道你暴露出去的 ws 应用的路径,有点类似我们经常用的@RequestMapping。比如你的启动端口是8080,而这个注解的值是ws,那我们就可以通过 ws://127.0.0.1:8080/ws 来连接你的应用

@OnOpen

当 websocket 建立连接成功后会触发这个注解修饰的方法,注意它有一个 Session 参数

@OnClose

当 websocket 建立的连接断开后会触发这个注解修饰的方法,注意它有一个 Session 参数

@OnMessage

当客户端发送消息到服务端时,会触发这个注解修改的方法,它有一个 String 入参表明客户端传入的值

@OnError

当 websocket 建立连接时出现异常会触发这个注解修饰的方法,注意它有一个 Session 参数

另外一点就是服务端如何发送消息给客户端,服务端发送消息必须通过上面说的 Session 类,通常是在@OnOpen 方法中,当连接成功后把 session 存入 Map 的 value,key 是与 session 对应的用户标识,当要发送的时候通过 key 获得 session 再发送,这里可以通过 session.getBasicRemote_().sendText(_) 来对客户端发送消息。

2. Spring封装

pom.xml

org.springframework.boot

spring-boot-starter-websocket

HttpAuthHandler

/*

* *

* * blog.coder4j.cn

* * Copyright (C) 2016-2019 All Rights Reserved.

*

*/

package cn.coder4j.study.example.websocket.handler;

import cn.coder4j.study.example.websocket.config.WsSessionManager;

import org.springframework.stereotype.Component;

import org.springframework.web.socket.CloseStatus;

import org.springframework.web.socket.TextMessage;

import org.springframework.web.socket.WebSocketSession;

import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.time.LocalDateTime;

/**

* @author buhao

* @version MyWSHandler.java, v 0.1 2019-10-17 17:10 buhao

*/

@Component

public class HttpAuthHandler extends TextWebSocketHandler {

/**

* socket 建立成功事件

*

* @param session

* @throws Exception

*/

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

Object token = session.getAttributes().get("token");

if (token != null) {

// 用户连接成功,放入在线用户缓存

WsSessionManager.add(token.toString(), session);

} else {

throw new RuntimeException("用户登录已经失效!");

}

}

/**

* 接收消息事件

*

* @param session

* @param message

* @throws Exception

*/

@Override

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

// 获得客户端传来的消息

String payload = message.getPayload();

Object token = session.getAttributes().get("token");

System.out.println("server 接收到 " + token + " 发送的 " + payload);

session.sendMessage(new TextMessage("server 发送给 " + token + " 消息 " + payload + " " + LocalDateTime.now().toString()));

}

/**

* socket 断开连接时

*

* @param session

* @param status

* @throws Exception

*/

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

Object token = session.getAttributes().get("token");

if (token != null) {

// 用户退出,移除缓存

WsSessionManager.remove(token.toString());

}

}

}

说明

通过继承 TextWebSocketHandler 类并覆盖相应方法,可以对 websocket 的事件进行处理,这里可以同原生注解的那几个注解连起来看

afterConnectionEstablished 方法是在 socket 连接成功后被触发,同原生注解里的 @OnOpen 功能

**afterConnectionClosed  **方法是在 socket 连接关闭后被触发,同原生注解里的 @OnClose 功能

**handleTextMessage **方法是在客户端发送信息时触发,同原生注解里的 @OnMessage 功能

WsSessionManager

/*

* *

* * blog.coder4j.cn

* * Copyright (C) 2016-2019 All Rights Reserved.

*

*/

package cn.coder4j.study.example.websocket.config;

import lombok.extern.slf4j.Slf4j;

import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;

import java.util.concurrent.ConcurrentHashMap;

/**

* @author buhao

* @version WsSessionManager.java, v 0.1 2019-10-22 10:24 buhao

*/

@Slf4j

public class WsSessionManager {

/**

* 保存连接 session 的地方

*/

private static ConcurrentHashMap SESSION_POOL = new ConcurrentHashMap<>();

/**

* 添加 session

*

* @param key

*/

public static void add(String key, WebSocketSession session) {

// 添加 session

SESSION_POOL.put(key, session);

}

/**

* 删除 session,会返回删除的 session

*

* @param key

* @return

*/

public static WebSocketSession remove(String key) {

// 删除 session

return SESSION_POOL.remove(key);

}

/**

* 删除并同步关闭连接

*

* @param key

*/

public static void removeAndClose(String key) {

WebSocketSession session = remove(key);

if (session != null) {

try {

// 关闭连接

session.close();

} catch (IOException e) {

// todo: 关闭出现异常处理

e.printStackTrace();

}

}

}

/**

* 获得 session

*

* @param key

* @return

*/

public static WebSocketSession get(String key) {

// 获得 session

return SESSION_POOL.get(key);

}

}

说明

这里简单通过 **ConcurrentHashMap **来实现了一个 session 池,用来保存已经登录的 web socket 的  session。前文提过,服务端发送消息给客户端必须要通过这个 session。

MyInterceptor

/*

* *

* * blog.coder4j.cn

* * Copyright (C) 2016-2019 All Rights Reserved.

*

*/

package cn.coder4j.study.example.websocket.interceptor;

import cn.hutool.core.util.StrUtil;

import cn.hutool.http.HttpUtil;

import org.springframework.http.server.ServerHttpRequest;

import org.springframework.http.server.ServerHttpResponse;

import org.springframework.stereotype.Component;

import org.springframework.web.socket.WebSocketHandler;

import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.HashMap;

import java.util.Map;

/**

* @author buhao

* @version MyInterceptor.java, v 0.1 2019-10-17 19:21 buhao

*/

@Component

public class MyInterceptor implements HandshakeInterceptor {

/**

* 握手前

*

* @param request

* @param response

* @param wsHandler

* @param attributes

* @return

* @throws Exception

*/

@Override

public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {

System.out.println("握手开始");

// 获得请求参数

HashMap paramMap = HttpUtil.decodeParamMap(request.getURI().getQuery(), "utf-8");

String uid = paramMap.get("token");

if (StrUtil.isNotBlank(uid)) {

// 放入属性域

attributes.put("token", uid);

System.out.println("用户 token " + uid + " 握手成功!");

return true;

}

System.out.println("用户登录已失效");

return false;

}

/**

* 握手后

*

* @param request

* @param response

* @param wsHandler

* @param exception

*/

@Override

public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {

System.out.println("握手完成");

}

}

说明

通过实现 HandshakeInterceptor 接口来定义握手拦截器,注意这里与上面 Handler 的事件是不同的,这里是建立握手时的事件,分为握手前与握手后,而  Handler 的事件是在握手成功后的基础上建立 socket 的连接。所以在如果把认证放在这个步骤相对来说最节省服务器资源。它主要有两个方法 beforeHandshake 与 **afterHandshake **,顾名思义一个在握手前触发,一个在握手后触发。

WebSocketConfig

/*

* *

* * blog.coder4j.cn

* * Copyright (C) 2016-2019 All Rights Reserved.

*

*/

package cn.coder4j.study.example.websocket.config;

import cn.coder4j.study.example.websocket.handler.HttpAuthHandler;

import cn.coder4j.study.example.websocket.interceptor.MyInterceptor;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.socket.config.annotation.EnableWebSocket;

import org.springframework.web.socket.config.annotation.WebSocketConfigurer;

import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**

* @author buhao

* @version WebSocketConfig.java, v 0.1 2019-10-17 15:43 buhao

*/

@Configuration

@EnableWebSocket

public class WebSocketConfig implements WebSocketConfigurer {

@Autowired

private HttpAuthHandler httpAuthHandler;

@Autowired

private MyInterceptor myInterceptor;

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

registry

.addHandler(httpAuthHandler, "myWS")

.addInterceptors(myInterceptor)

.setAllowedOrigins("*");

}

}

说明

通过实现 WebSocketConfigurer 类并覆盖相应的方法进行 websocket 的配置。我们主要覆盖 registerWebSocketHandlers 这个方法。通过向 WebSocketHandlerRegistry 设置不同参数来进行配置。其中 **addHandler 方法添加我们上面的写的 ws 的  handler 处理类,第二个参数是你暴露出的 ws 路径。addInterceptors 添加我们写的握手过滤器。setAllowedOrigins("*") **这个是关闭跨域校验,方便本地调试,线上推荐打开。

3. TIO

pom.xml

org.t-io

tio-websocket-spring-boot-starter

3.5.5.v20191010-RELEASE

application.xml

tio:

websocket:

server:

port: 8989

说明

这里只配置了 ws 的启动端口,还有很多配置,可以通过结尾给的链接去寻找

MyHandler

/*

* *

* * blog.coder4j.cn

* * Copyright (C) 2016-2019 All Rights Reserved.

*

*/

package cn.coder4j.study.example.websocket.handler;

import org.springframework.stereotype.Component;

import org.tio.core.ChannelContext;

import org.tio.http.common.HttpRequest;

import org.tio.http.common.HttpResponse;

import org.tio.websocket.common.WsRequest;

import org.tio.websocket.server.handler.IWsMsgHandler;

/**

* @author buhao

* @version MyHandler.java, v 0.1 2019-10-21 14:39 buhao

*/

@Component

public class MyHandler implements IWsMsgHandler {

/**

* 握手

*

* @param httpRequest

* @param httpResponse

* @param channelContext

* @return

* @throws Exception

*/

@Override

public HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {

return httpResponse;

}

/**

* 握手成功

*

* @param httpRequest

* @param httpResponse

* @param channelContext

* @throws Exception

*/

@Override

public void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {

System.out.println("握手成功");

}

/**

* 接收二进制文件

*

* @param wsRequest

* @param bytes

* @param channelContext

* @return

* @throws Exception

*/

@Override

public Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {

return null;

}

/**

* 断开连接

*

* @param wsRequest

* @param bytes

* @param channelContext

* @return

* @throws Exception

*/

@Override

public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {

System.out.println("关闭连接");

return null;

}

/**

* 接收消息

*

* @param wsRequest

* @param s

* @param channelContext

* @return

* @throws Exception

*/

@Override

public Object onText(WsRequest wsRequest, String s, ChannelContext channelContext) throws Exception {

System.out.println("接收文本消息:" + s);

return "success";

}

}

说明

这个同上个例子中的 handler 很像,也是通过实现接口覆盖方法来进行事件处理,实现的接口是IWsMsgHandler,它的方法功能如下

handshake

在握手的时候触发

onAfterHandshaked

在握手成功后触发

onBytes

客户端发送二进制消息触发

onClose

客户端关闭连接时触发

onText

客户端发送文本消息触发

StudyWebsocketExampleApplication

/*

* *

* * blog.coder4j.cn

* * Copyright (C) 2016-2019 All Rights Reserved.

*

*/

package cn.coder4j.study.example.websocket;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.tio.websocket.starter.EnableTioWebSocketServer;

@SpringBootApplication

@EnableTioWebSocketServer

public class StudyWebsocketExampleApplication {

public static void main(String[] args) {

SpringApplication.run(StudyWebsocketExampleApplication.class, args);

}

}

说明

这个类的名称不重要,它其实是你的 spring boot 启动类,只要记得加上@EnableTioWebSocketServer注解就可以了

STOMP

pom.xml

org.springframework.boot

spring-boot-starter-websocket

WebSocketConfig

/*

* *

* * blog.coder4j.cn

* * Copyright (C) 2016-2019 All Rights Reserved.

*

*/

package cn.coder4j.study.example.websocket.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.messaging.simp.config.MessageBrokerRegistry;

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;

import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**

* @author buhao

* @version WebSocketConfig.java, v 0.1 2019-10-21 16:32 buhao

*/

@Configuration

@EnableWebSocketMessageBroker

public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override

public void registerStompEndpoints(StompEndpointRegistry registry) {

// 配置客户端尝试连接地址

registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();

}

@Override

public void configureMessageBroker(MessageBrokerRegistry registry) {

// 设置广播节点

registry.enableSimpleBroker("/topic", "/user");

// 客户端向服务端发送消息需有/app 前缀

registry.setApplicationDestinationPrefixes("/app");

// 指定用户发送(一对一)的前缀 /user/

registry.setUserDestinationPrefix("/user/");

}

}

说明

通过实现 WebSocketMessageBrokerConfigurer 接口和加上@EnableWebSocketMessageBroker来进行 stomp 的配置与注解扫描。

其中覆盖 registerStompEndpoints 方法来设置暴露的 stomp 的路径,其它一些跨域、客户端之类的设置。

覆盖 **configureMessageBroker **方法来进行节点的配置。

其中 **enableSimpleBroker **配置的广播节点,也就是服务端发送消息,客户端订阅就能接收消息的节点。

覆盖**setApplicationDestinationPrefixes **方法,设置客户端向服务端发送消息的节点。

覆盖 setUserDestinationPrefix 方法,设置一对一通信的节点。

WSController

/*

* *

* * blog.coder4j.cn

* * Copyright (C) 2016-2019 All Rights Reserved.

*

*/

package cn.coder4j.study.example.websocket.controller;

import cn.coder4j.study.example.websocket.model.RequestMessage;

import cn.coder4j.study.example.websocket.model.ResponseMessage;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.messaging.handler.annotation.MessageMapping;

import org.springframework.messaging.handler.annotation.SendTo;

import org.springframework.messaging.simp.SimpMessagingTemplate;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.ResponseBody;

/**

* @author buhao

* @version WSController.java, v 0.1 2019-10-21 17:22 buhao

*/

@Controller

public class WSController {

@Autowired

private SimpMessagingTemplate simpMessagingTemplate;

@MessageMapping("/hello")

@SendTo("/topic/hello")

public ResponseMessage hello(RequestMessage requestMessage) {

System.out.println("接收消息:" + requestMessage);

return new ResponseMessage("服务端接收到你发的:" + requestMessage);

}

@GetMapping("/sendMsgByUser")

public @ResponseBody

Object sendMsgByUser(String token, String msg) {

simpMessagingTemplate.convertAndSendToUser(token, "/msg", msg);

return "success";

}

@GetMapping("/sendMsgByAll")

public @ResponseBody

Object sendMsgByAll(String msg) {

simpMessagingTemplate.convertAndSend("/topic", msg);

return "success";

}

@GetMapping("/test")

public String test() {

return "test-stomp.html";

}

}

说明

通过 @MessageMapping 来暴露节点路径,有点类似 @RequestMapping。注意这里虽然写的是 hello ,但是我们客户端调用的真正地址是** /app/hello。 因为我们在上面的 config 里配置了registry.setApplicationDestinationPrefixes("/app")**。

@SendTo这个注解会把返回值的内容发送给订阅了 /topic/hello 的客户端,与之类似的还有一个@SendToUser 只不过他是发送给用户端一对一通信的。这两个注解一般是应答时响应的,如果服务端主动发送消息可以通过 simpMessagingTemplate类的convertAndSend方法。注意 simpMessagingTemplate.convertAndSendToUser(token, "/msg", msg) ,联系到我们上文配置的 registry.setUserDestinationPrefix("/user/"),这里客户端订阅的是/user/{token}/msg,千万不要搞错。

Session 共享的问题

上面反复提到一个问题就是,服务端如果要主动发送消息给客户端一定要用到 session。而大家都知道的是 session 这个东西是不跨 jvm 的。如果有多台服务器,在 http 请求的情况下,我们可以通过把 session 放入缓存中间件中来共享解决这个问题,通过 spring session 几条配置就解决了。但是 web socket  不可以。他的 session 是不能序列化的,当然这样设计的目的不是为了为难你,而是出于对 http 与 web socket 请求的差异导致的。

目前网上找到的最简单方案就是通过 redis 订阅广播的形式,主要代码跟第二种方式差不多,你要在本地放个 map 保存请求的 session。也就是说每台服务器都会保存与他连接的 session 于本地。然后发消息的地方要修改,并不是现在这样直接发送,而通过 redis 的订阅机制。服务器要发消息的时候,你通过 redis 广播这条消息,所有订阅的服务端都会收到这个消息,然后本地尝试发送。最后肯定只有有这个对应用户 session 的那台才能发送出去。

如何选择

如果你在使用 tio,那推荐使用 tio 的集成。因为它已经实现了很多功能,包括上面说的通过 redis 的 session 共享,只要加几个配置就可以了。但是 tio 是半开源,文档是需要收费的。如果没有使用,那就忘了他。

如果你的业务要求比较灵活多变,推荐使用前两种,更推荐第二种 Spring 封装的形式。

如果只是简单的服务器双向通信,推荐 stomp 的形式,因为他更容易规范使用。

其它

websocket 在线验证

写完服务端代码后想调试,但是不会前端代码怎么办,点这里,这是一个在线的 websocket 客户端,功能完全够我们调试了。

stomp 验证

这个没找到在线版的,但是网上有很多 demo 可以下载到本地进行调试,也可以通过后文的连接找到。

另外由于篇幅有限,并不能放上所有代码,但是测试代码全都上传 gitlab,保证可以正常运行,可以在 这里 找到

参考链接

java websocket注解_【websocket】spring boot 集成 websocket 的四种方式相关推荐

  1. spring boot 集成 websocket 实现消息主动推送

    前言 http协议是无状态协议,每次请求都不知道前面发生了什么,而且只可以由浏览器端请求服务器端,而不能由服务器去主动通知浏览器端,是单向的,在很多场景就不适合,比如实时的推送,消息通知或者股票等信息 ...

  2. SpringBoot2.x系列教程(四十五)Spring Boot集成WebSocket实现技术交流群功能

    在上篇文章中,我们了解了WebSocket的基本功能及相关概念.本篇文章中我们以具体的实例来演示,在Spring Boot中整合WebSocket,同时实现一个场景的业务场景功能. 针对在Spring ...

  3. java中四种注入注解,Spring中依赖注入的四种方式

    在Spring容器中为一个bean配置依赖注入有三种方式: · 使用属性的setter方法注入  这是最常用的方式: · 使用构造器注入: · 使用Filed注入(用于注解方式). 使用属性的sett ...

  4. Spring Boot项目启动的几种方式

    Spring Boot项目启动的几种方式 方式一:右击启动或者点击intellij右上角的启动按钮 我们访问下浏览器看一下效果 方式二:利用maven启动 我们先进入到项目文件下,然后执行命令   m ...

  5. Spring Boot 集成 WebSocket通信信息推送!

    一.什么是websocket? WebSocket 协议是基于 TCP 的一种新的网络协议. 它实现了客户端与服务器之间的全双工通信,学过计算机网络都知道,既然是全双工,就说明了服务器可以主动发送信息 ...

  6. 招聘管理系统软件java源码_基于Spring Boot的java开源招聘源码-铭阳招聘管理系统...

    铭阳招聘管理系统 铭阳招聘管理系统,采用流行的框架Spring Boot+mybatis+ehcache开发,实现了权限管理,solr全文搜索引擎,系统具执行效率高.模板自由切换.后台管理功能灵活等诸 ...

  7. Spring Boot 实现定时任务的 4 种方式

    点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 作者:Wan QingHua wanqhblog.top/2018/02/01/SpringB ...

  8. Spring Boot 注册 Servlet 的3种方式

    一.Spring Boot 注册 Spring Boot 提供了 ServletRegistrationBean, FilterRegistrationBean, ServletListenerReg ...

  9. Spring Boot 获取 Bean 的 3 种方式!还有谁不会?

    作者 | chilx 来源 | https://blog.csdn.net/showchi/article/details/97005720 注意:调用者要被spring管理 方式一 注解@PostC ...

最新文章

  1. Linked List Cycle II
  2. 3分钟内快速部署MySQL5.6.35数据库实践
  3. 选择NLP供应商之前需要提出的一些关键问题
  4. python基于什么语言-一种基于Python语言的EDA开发平台及其使用方法与流程
  5. Spring注解使用方法
  6. 启明云端分享|ESP8089 pin to pin 替代RTL8189,供应稳定、性价比高!
  7. catcti监控linux主机,CentOS7搭建Prometheus 监控Linux主机
  8. ubuntu16.04下pycharm中无法使用中文输入法
  9. linux恢复fat文件系统,使用‘fsck’修复Linux中文件系统错误的方法
  10. C语言实现井子格游戏
  11. html 在线图片压缩,JS HTML图片显示Canvas 压缩功能
  12. MacOS系统下matplotlib中SimHei中文字体无法启动解决办法
  13. iPhone通讯录整理及vcf-CSV互转
  14. 通用计算机不能直接硬件乘法,2018年4月自考《计算机组成原理》真题
  15. Flyme-Substratum主题
  16. python用什么方式可以打印换行字符串_字符串是一个连续的字符序列,用________方式打印出可以换行的字符串。...
  17. [首发] 多方位玩转“地平线新发布AIoT开发板——旭日X3派(Sunrise x3 Pi)” 插电!开机!轻松秒杀!
  18. linux mtd 用法,Linux mtd与ubi关系详解,ubi使用命令总结
  19. 线性代数 --- 线性代数基本定理下(四个基本子空间两两正交且互为正交补)
  20. 它号称 Python 中性能最高的异步 Web 框架:超详细 Sanic 入门指南!

热门文章

  1. 微信分享链接,如何自定义图片,标题,内容介绍
  2. 如何在阿里云上安全的存放您的配置
  3. 云计算作业001-电脑配置
  4. MySql类似Oracle的dual虚拟表
  5. 安装CleanMyMac 3提示软件已损坏
  6. PostgreSQL的中文拼音排序
  7. GLPI+OCS、SmartIT、LANDesk比较
  8. spring ,springmvc的常用标签注解
  9. Java8 函数式编程之函数接口(上)
  10. C# 将链表存入二进制文件及读取二进制文件得到链表示例