从阿里云服务器购买、域名购买、SSL免费购买。
一 WebSocket实战
1.1 认识WebSocket
上图说明
-
发送连接请求
客户端通过ws://host:port/ 的请求地址发起WebSocket请求连接。由JavaScript实现
WebSocket API与服务器建立WebSocket连接。host服务器ip,port为端口。 -
握手
服务器端接受请求后,会解析请求头信息,根据升级协议判断请求是否为WebSocket请求,并取出请求信息中的
Sec-WebSocket-Key字段的数据。按照某种算法生成一个新的字符串序列放到请求头Sec-WebSocket-Accept中。
Sec-WebSocket-Accept:服务器接受客户端HTTP协议升级证明。 -
建立WebSocket连接
客户端接受服务器的响应后,同样会解析请求头信息,取出Sec-WebSocket-Accept字段。
并用服务器端相同的算法对之前Sec-WebSocket-Accept的数据处理,看看处理后的和取出
的Sec-WebSocket-Accept对比。一样连接建立成功,不一样建立失败。
1.2 HTTP和WebSocket
上图说明
上图可以看出,HTTP每次请求都需要建立连接。WebSocket类似一个长链接,一旦建立后,后续数据
都是以帧序列方式传递。
-
HTTP和WebSocket关系
相同点:HTTP和WebSocket都是可靠的传输协议,都是应用层协议。
不同点:WebSocket是双向通信协议,模拟socket协议,可以双向发送和接受数据。HTTP是单向的。 -
WebSocket建立连接细节
WebSocket建立握手,数据是通过HTTP传输的。但是建立连接后,在真正传输的时候不需要HTTP协议的。
WebSocket中,浏览器和服务器进行一个握手之后,然后单独建立一条TCP的通信通道,进行数据传递。 -
WebSocket优点
双工通信代替轮询。可以做即时通信和消息推送。
1.3 SpringBoot集成WebSocket
- 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 开启WebSocket支持端点
@Configuration
public class Config {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
- 创建server核心类
@ServerEndpoint("/ws/{userId}")
@Component
public class WebSocketServer {
//日志
static Log log = LogFactory.getLog(WebSocketServer.class);
//在线数量
private static final AtomicInteger onlineCount = new AtomicInteger(0);
//处理客户端连接socket
private static Map<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
//会话信息
private Session session;
//用户信息
private String userId = "";
/*
* @Description: 打开WebSokcet连接
*/
@OnOpen
public void onOPen(@PathParam("userId") String userId, Session session) {
//处理session和用户信息
this.session = session;
this.userId = userId;
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
webSocketMap.put(userId, this);
} else {
webSocketMap.put(userId, this);
//增加在线人数
addOnlineCount();
}
try {
//处理连接成功消息的发送
sendMessage("Server>>>>远程WebSoket连接成功");
log.info("用户" + userId + "成功连接,当前的在线人数为" + getOnlineCount());
} catch (Exception e) {
e.printStackTrace();
}
}
/*
* @Description: 关闭连接
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
subOnlineCount();
}
log.info("用户退出....");
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount.decrementAndGet();
}
/*
* @Description:消息中转
*/
@OnMessage
public void onMessage(String message, Session session) {
if (StringUtils.isNotEmpty(message)) {
try {
//解析消息
JSONObject jsonObject = JSON.parseObject(message);
String toUserId = jsonObject.getString("toUserId");
String msg = jsonObject.getString("msg");
if (StringUtils.isNotEmpty(toUserId) && webSocketMap.containsKey(toUserId)) {
webSocketMap.get(toUserId).sendMessage(msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
* @Description: 服务端向客户端发送数据
*/
public void sendMessage(String s) throws IOException {
this.session.getBasicRemote().sendText(s);
}
/*
* @Description: 获取在线人数的数量
*/
public static synchronized AtomicInteger getOnlineCount() {
return onlineCount;
}
/*
* @Description: 增加在线人数
*/
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount.incrementAndGet();
}
/*
* @Description: 服务器消息推送
*/
public static boolean sendInfo(String message, @PathParam("userId") String userId) throws IOException {
boolean flag=true;
if (StringUtils.isNotEmpty(userId) && webSocketMap.containsKey(userId)) {
webSocketMap.get(userId).sendMessage(message);
} else {
log.error("用户" + userId + "不在线");
flag=false;
}
return flag;
}
}
- 创建控制器
@RestController
public class WebSocketController {
@RequestMapping("im")
public ModelAndView page() {
return new ModelAndView("ws");
}
/*
* @Description: 消息推送
*/
@RequestMapping("/push/{toUserId}")
public ResponseEntity<String> pushToWeb(String message, @PathVariable String toUserId) throws Exception {
boolean flag = WebSocketServer.sendInfo(message, toUserId);
return flag == true ? ResponseEntity.ok("消息推送成功...") : ResponseEntity.ok("消息推送失败,用户不在线...");
}
}
- 创建消息发送HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="../css/index.css" rel="stylesheet">
</head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
var socket;
function openWebSocket() {
if (typeof(WebSocket) == "undefined") {
console.log("对不起,您的浏览器不支持WebSocket");
} else {
var webSocketUrl = "ws://localhost/ws/" + $("#userId").val();
if (socket != null) {
socket.close();
socket = null;
}
socket = new WebSocket(webSocketUrl);
//打开
socket.onopen = function () {
console.log("Client>>>>WebSocket已打开");
};
//获取消息
socket.onmessage = function (msg) {
console.log(msg.data);
$("#msg").val(msg.data)
};
//关闭
socket.onclose = function () {
console.log("Client>>>>WebSocket已关闭");
};
//发生错误
socket.onerror = function () {
console.log("Client>>>>WebSocket发生了错误");
}
}
}
function sendMessage() {
if (typeof(WebSocket) == "undefined") {
console.log("对不起,您的浏览器不支持WebSocket");
} else {
socket.send('{"toUserId":"' + $("#toUserId").val() + '","msg":"' + $("#msg").val() + '"}');
}
}
</script>
<body>
<div id="panel">
<div class="panel-header">
<h2>即时通讯IM</h2>
</div>
<div class="panel-content">
<div class="user-pwd">
<button class="btn-user">发</button>
<input id="userId" name="userId" type="text" value="张三">
</div>
<div class="user-pwd">
<button class="btn-user">收</button>
<input id="toUserId" name="toUserId" type="text" value="李四">
</div>
<div class="user-msg">
<input id="msg" name="msg" type="text" value="">
</div>
<div class="panel-footer">
<button class="login-btn" onclick="openWebSocket()" >连接WebSocket</button>
<button class="login-btn" onclick="sendMessage()">发送消息</button>
</div>
</div>
</body>
</html>
- 访问路径
http://localhost/im
-
WebSocket建立连接请求头分析
image
WebSocket利用HTTP建立我握手连接,必须由浏览器发起。相对于HTTP协议多了几个东西,
告诉Apache、Nginx等服务器,本次发起的是Websocket协议。
a GET请求的地址不是类似http/,而是以ws://开头的地址;
b 请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接;
c Sec-WebSocket-Key是由浏览器随机生成的,是用于标识这个连接,并非用于加密数据;
d Sec-WebSocket-Version指定了WebSocket的协议版本;
e Sec-WebSocket-Extensions:请求扩展;
f code为101代表了服务端已经理解了客户端请求。
1.4 自签名证书HTTPS开发
1.4.1 生成keystore证书
- JDK中keytool是一个证书管理工具,可以生成自签名证书
keytool -genkey -alias czbk -keypass 123456 -keyalg RSA -keysize 1024 -validity
365 -keystore c:/czbk.keystore -storepass 123456
命令解释
keytool
-genkey
-alias tomcat(别名)
-keypass 123456(别名密码)
-keyalg RSA(生证书的算法名称,RSA是一种非对称加密算法)
-keysize 1024(密钥长度,证书大小)
-validity 365(证书有效期,天单位)
-keystore c:/czbk.keystore(指定生成证书的位置和证书名称)
-storepass 123456(获取keystore信息的密码)
- storetype (指定密钥仓库类型)
-
查看生成文件
image
并将文件复制到spring boot项目中的resources目录中。
-
配置application.properties
# 端口
http.port=7777
server.port=8888
# 指定签名文件,对应生成的密钥库文件
server.ssl.key-store=classpath:czbk.keystore
# 指定签名密码,设置的密钥库指令
server.ssl.key-store-password=123456
# 指定密钥仓库类型,JKS
server.ssl.key-store-type=JKS
# 指定别名,生成密钥库的时候进行了设定
server.ssl.key-alias=czbk
- 增加配置类
@Configuration
public class HttpRedirectHttps {
@Value("${http.port}")
Integer httpPort;
@Value("${server.port}")
Integer httpsPort;
/*
* @Description: http重定向到https
* @Method: servletWebServerFactory
* @Param: []
* @Update:
* @since: 1.0.0
* @Return: org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
*
*/
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcatServletWebServerFactory.addAdditionalTomcatConnectors(createConnector());
return tomcatServletWebServerFactory;
}
public Connector createConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(httpPort);
connector.setSecure(false);
connector.setRedirectPort(httpsPort);
return connector;
}
}
- 验证集成结果
http://localhost:7777/im 跳转到 https://localhost:8888/im
上图说明
我们自己生成的https证书不被谷歌浏览器认可。更换浏览器即可
1.5 Nginx代理SSL
-
好处
springboot程序不需要加入ssl任何配置。
解决http重定向https
解决Wss服务问题
Nginx版本必须大于1.3 -
关于nginx
自1.3 版本开始,Nginx就支持 WebSocket,并且可以为 WebSocket 应用程序做反向代理和负载均衡。
WebSocket 和 HTTP 是两种不同的协议,但是 WebSocket 中的握手和 HTTP 中的握手兼容,它使用
HTTP 中的 Upgrade 协议头将连接从 HTTP 升级到 WebSocket,当客户端发过来一个 Connection:
Upgrade请求头时,其实Nginx是不知道的。所以,当 Nginx 代理服务器拦截到一个客户端发来的
Upgrade 请求时,需要我们显式的配置Connection、Upgrade头信息,并使用 101(交换协议)返回响
应,在客户端、代理服务器和后端应用服务之间建立隧道来支持 WebSocket。
- Nginx流程
使用 Nginx 反向代理来解决 WebSocket 的 wss 服务问题,即客户端通过 Wss 协议连接 Nginx 然后
Nginx 通过 Ws 协议和 Server 通讯。也就是说 Nginx 负责通讯加解密,Nginx 到 Server 是明文的。
Nginx配置文件
#********分割线******************
events {
worker_connections 1024;
}
#********分割线******************
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;}
#********分割线******************
server {
listen 80;
server_name websocket.nginx.com;
add_header Strict-Transport-Security max-age=15768000;
return 301 https://$server_name$request_uri;
}
#********分割线******************
server {
listen 443 ssl;
server_name websocket.nginx.com
ssl on;
ssl_certificate c:\czbk.crt;
ssl_certificate_key c:\czbk.key;
ssl_session_timeout 5m;
ssl_protocols SSLv3 SSLv2 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCMSHA256:
ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_prefer_server_ciphers on;
#********页面http访问的时候也可以使用/im*****************
#location /im/ {
#proxy_pass http://localhost:8888/im;
#}
#********http跳转与wss协议调用(自动判断请求协议)******************
location / {
proxy_pass http://localhost:8888;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Real-IP $remote_addr;
}
}
#********分割线******************
}
map指令的作用
#该作用主要是根据客户端请求中$http_upgrade 的值,来构造改变$connection_upgrade的值,即根
据变量$http_upgrade的值创建新的变量$connection_upgrade,
#创建的规则就是{}里面的东西。其中的规则没有做匹配,因此使用默认的,即 $connection_upgrade
的值会一直是 upgrade。然后如果 $http_upgrade为空字符串的话,那值会是 close。
核心配置
ssl_certificate 证书其实是个公钥,它会被发送到连接服务器的每个客户端
ssl_certificate_key 私钥是用来解密的,所以它的权限要得到保护但nginx的主进程能够读取。
ssl_session_timeout : 客户端可以重用会话缓存中ssl参数的过期时间
ssl_protocols指令用于支持加密协议
ssl_ciphers选择加密套件,不同的浏览器所支持的套件(和顺序)可能会不同。这里指定的是
OpenSSL库能够识别的写法,你可以通过 openssl -v cipher 'RC4:HIGH:!aNULL:!MD5'(后面是你所指
定的套件加密算法) 来看所支持算法。
加密套件 之间用冒号分隔,加密套件 前有感叹号的表示必须废弃。
ssl_prefer_server_ciphers on设置协商加密算法时,优先使用我们服务端的加密套件,而不是客户端浏
览器的加密套件
访问流程
1、http://websocket.nginx.com/im/访问进入到 listen 80;
2、接着跳转到listen 443
3、进入到443后分别(wss或者http)进行跳转
重载 Nginx 服务
主流数字证书都有哪些格式?
一般来说,主流的Web服务软件,通常都基于两种基础密码库:OpenSSL和Java。
Tomcat、Weblogic、JBoss等,使用Java提供的密码库。通过Java的Keytool工具,生成Java
Keystore(JKS)格式的证书文件。
Apache、Nginx等,使用OpenSSL提供的密码库,生成PEM、KEY、CRT等格式的证书文件
此处我们需要crt和key文件
我们需要将证书kestore导出crt和key
1、使用kse-543转p12
2、通过P12生成crt和key文件
2 阿里云HTTPS开发
2.1 准备工作
- 准备工作
a 阿里云ecs一个
b 域名一个(www.itheima.cloud)
c ca证书一份(用来支持https)(需要绑定域名)
d 本地打包好的springboot项目 (需要打包上传到阿里云)
e ftp客户端一个,用来把jar传到阿里云服务器上
- 阿里云域名申请
https://mi.aliyun.com/
-
选择域名直接购买
image
image
- 查看域名状态
https://dc.console.aliyun.com/next/index?
spm=5176.100251.recommends.ddomain.6ffe4f15tozYLa#/domain/details/info?
saleId=DT49H3EX462ZAYP&domain=itheima.cloud
-
whois结果
image
- 阿里云ECS服务器申请
https://www.aliyun.com/activity/618/index?
spm=5176.12825654.a9ylfrljh.d111.e9392c4aU65uab&scm=20140722.2188.2.2170
- 支付成功,查看ECS实例
https://ecs.console.aliyun.com/?spm=5176.2020520132.productsgrouped.
decs.a1597218YHc0M5#/server/region/cn-beijing
- 远程登录--重置密码
阿里云服务器购买之后,新的实例需要设置root登录密码之后才能正常操作,不然就登录不了。重置实例
登录密码的时候,适用于在新创建时未设置密码或者忘记密码的情况。对于正在运行的实例,需要在重置
实例登录密码之后重启实例才能使新的密码生效
-
通过外网IP进行连接
image
2.2 阿里云SSL证书申请
https://common-buy.aliyun.com/?
spm=5176.7968328.1266638..213d1232uExCSm&commodityCode=cas#/buy
-
选择SSL证书(应用安全)
image
-
购买证书
image
-
购买免费域名(单域名---DV/SSL---免费版)
image
-
开始支付
image
-
支付成功
image
- 支付成功后在
image
- 点击证书申请,进行【证书与域名绑定】
image
image
image
2.3 域名与ECS服务器绑定
https://dns.console.aliyun.com/?
spm=5176.100251.111252.22.3a8b4f15HNfTsl#/dns/setting/itheima.cloud
-
DNS解析
image
-
点击确定后出现www和@
image
2.4 SpringBoot部署到阿里云
-
阿里云远程部署
image
- 配置文件修改(证书路径要带上 classpath)
http:
port: 7777
server:
# 端口 使用HTTPS默认端口
port: 443
#HTTPS加密配置
ssl:
#证书路径
key-store: classpath:4108720_www.itheima.cloud.pfx
#证书密码
key-store-password: 5nLB0iXq
- centos7 下杀死占用端口的进程
# 根据端口号得到其占用的进程的详细信息
netstat -lnp|grep 443
# 查看进程的详细信息
ps "pid"
#直接杀掉占用端口的进程 -9是强制关闭
kill -9 "pid"
或者
netstat -tunlp|grep 443
jobs 查看nohup运行的程序
ps -ef | grep java
kill- 9 id
- 将springboot项目打包(使用idea的packge),并启动
nohup java -jar itheima-websocket-aliyun-https-1.0-SNAPSHOT.jar &
- 访问阿里云HTTPS应用程序(安全组配置)
https://ecs.console.aliyun.com/?spm=a2c1d.8251892.recommends.decs.10565b76fQAfCc#/ser
ver/region/cn-beijing
ip访问
http://101.201.232.138:7777/im
域名访问
https://www.itheima.cloud/im
注意:
第一次部署证书的时候,是可以访问的,大约过滤两分钟后,被阿里云监测到后就无法访问了
因为域名尚未备案成功
转到http访问,就会有提示尚未备案的提示
网友评论