概念:HTTP协议是一种无状态,无连接,单向的应用层协议,它采用的是请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理,一个http响应对应着一个响应。
缺点:因为它的特性是一个响应对应着一个请求,所以,服务器不能主动发送消息给客户端(浏览器)这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数web应用程序通过频繁的异步AJAX请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者HTTP连接始终打开)。
无状态:
? 无状态是指协议对事物处理没有记忆能力,服务器不知道客户端是什么状态。即我们给服务器发送HTTP请求之后,服务器根据请求 会给我们发送数据过来,但是发送完,不会记录任何信息。
优点:解放了服务器,每一次请求“点到为止”不会造成不必要连接的占用
**缺点:**每次请求会传输大量重复的内容信息。
无连接:
? 它的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
? 优点:可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户端适合使用长连接。
? 缺点:client端一般不会主动关闭连接,当client与server之间的连接一直不关闭,随着客户端连接越来越多,server会保持过多连接。server端需要采取一些策略来控制
? 使用场景:长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。
? 优点:对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。
? 缺点:客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。需要频繁的建立连接
? 使用场景:WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接
WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。WebSocket 是一种网络通信协议, 由HTML5提出的一种在单个 TCP 连接上进行全双工通讯的协议。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,具有持久化,一般应用在:聊天室,股票基金报价,协同办公等等…其他特点包括:
WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。
全双工:就是可以服务器和客户端进行互发消息,既可以发送数据也可以接收数据.它允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力;在同一时间可以同时接受和发送信息,实现双向通信,举例:电话通信。一个很宽的
半双工: 半双工数据传输允许数据在两个方向上传输,但是,在同一时间,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;在同一时间只可以有一方接受或发送信息,可以实现双向通信。举例:对讲机,独木桥。
单工: 单工是指数据传输只支持数据在一个方向上传输;在同一时间只有一方能接受或发送信息,不能实现双向通信,举例:电视,广播。
本协议有两部分:握手和数据传输(握手是基于http协议的)
客户端发送的握手请求
GET /chat HTTP/1.1
Host: XXX.com
Connection: Upgrade//一个申请协议升级的 HTTP 请求
Upgrade: websocket//告诉服务器给升级到websocket协议
Sec-WebSocket-Version: 13// 告诉服务器所使用的协议版本
Sec-WebSocket-key: XXXX//是base64加密的字符串 浏览器自动生成
服务端响应客户端握手请求
HTTP/1.1 101 Switching Protocols //返回101状态码
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= //对Sec-WebSocket-key的加密 同意握手建立链接 客户端收到 Sec-WebSocket-Accept后 将本地的Sec-WebSocket-key 编码做一个对比来验证
字段说明:
头名称 | 说明 |
---|---|
Connection:Upgrade | 标识该HTTP请求是一个协议升级请求 |
Upgrade:WebSocket | 协议升级为WebSocket协议 |
Sec-WebSocket-Version:13 | 客户端支持WebSocket的版本 |
Sec-WebSocket-Key: | 客户端采用base64编码的24位随机字符序列,服务器接受客户端HTTP协议升级的证明。要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答 |
Sec-WebSocket-Extensions | 协议扩展类型 |
实现WebSocket的Web浏览器将通过WebSocket对象公开所有必需的客户端功能(主要指支持Html5的浏览器)。
以下API用于创建WebSocket对象:
var ws = new WebSocket(url);//参数url格式说明:ws://ip地址:端口号/资源名称
WebSocket对象的相关事件
事件 | 事件处理程序 | 描述 |
---|---|---|
open | Socket.onopen | 连接建立时触发 |
message | Socket.onmessage | 客户端接收服务端数据时触发 |
error | Socket.onerror | 通信发生错误时触发 |
close | Socket.onclose | 连接关闭时触发 |
WebSocket对象的相关方法:
方法 | 描述 |
---|---|
send() | 使用连接发送数据 |
Tomcat的7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356)。
Java WebSocket应用由一系列的websocketEndpoint组成。Endpoint是一个Java对象,代表 WebSocket链接的一端,对于服务端,我们可以视为处理具体 WebSocket消息的接口,就像Servlet之与http请求一样。可以通过注解的方式定义Endpoint.
Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法,规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期如下:
注解 | 含义描述 |
---|---|
@OnClose | 当会话关闭时调用。 |
@OnOpen | 当开启一个新的会话时调用,该方法是客户端与服务端握手成功后调用的方法。 |
@OnError | 当连接过程中异常时调用。 |
服务端如何接受客户端发送的数据呢?
通过为Session添加MessageHandler消息处理器来接受消息,当采用注解方式定义Endpoint时,我们还可以通过@OnMessage注解指定接收消息的方法。
服务端如何推送数据给客户端呢?
发送消息则由RemoteEndpoint完成,其实例由Session维护,根据使用情况,我们可以通过Session.getBasicRemote获取同步消息发送的实例,然后调用其sendXxx()方法就可以发送消息,可以通过Session.getAsyncRemote获取异步消息发送实例。
<dependencies>
<dependency>
<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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.66</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
ServerEndpointExporter
官方的解释是:当注入这个对象的时候,可以将webSocket对象注入Spring容器中。
session
webSocket会话对象,发送消息时需要
开启websocket支持
package com.cto.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import javax.websocket.server.ServerEndpoint;
//开启websocket支持
@Configuration
public class WebSocketConfig {
@Bean
public static ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
编写服务端的代码
这就是重点了,核心都在这里。
因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketService其实就相当于一个ws协议的Controller
直接@ServerEndpoint(“/imserver/{userId}”) 、@Component启用即可,然后在里面实现@OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等方法。
新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便IM之间对userId进行推送消息。单机版实现到这里就可以。
package com.linjiu.config;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ConcurrentModificationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/websocket/{userId}")
@Component
public class WebSocketServer {
private static int onlineCount=0;//连接人数
private Session session;//建立连接
private static String userId="";//用户的昵称
//所有用户的连接信息,CopyOnWriteArraySet:线程安全的无序的集合
private static CopyOnWriteArraySet<WebSocketService> sendAll = new CopyOnWriteArraySet<>();
//设置get方法来获取连接人数
public static int getOnlineCount(){
return onlineCount;
}
//用户连接的时候自增
//synchronized:同步锁
public synchronized void addOnlineCount(){
WebSocketServer.onlineCount++;
}
//用户连接的时候自减
public synchronized void subOnlineCount(){
WebSocketServer.onlineCount--;
}
/**
* 建立连接的方法
* @param session 用户连接的对象
* @param userId 用户的ID
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId")String userId){
this.session = session;
this.userId = userId;
sendAll.add(this); //把用户的昵称连接会话和信息存到sendAll里面
addOnlineCount(); //调用上方自增的方法
System.out.println("有用户连接,当前连接人数为:"+getOnlineCount());
}
/**
* 用户关闭连接
*/
@OnClose
public void onClose(){
sendAll.remove(this);
subOnlineCount(); //调用上方自减的方法
System.out.println("有用户断开连接,当前人数为:"+getOnlineCount());
}
/**用户连接异常
*
* @param throwable 抛出异常
*/
@OnError
public void onError(Throwable throwable){
throwable.printStackTrace();
System.out.println("连接错误");
}
/**
*实现服务器主动推送消息
* @param message 消息
* @throws IOException
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 收到客户端消息调用的方法
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message){
System.out.println("用户"+userId+message);
for (WebSocketServer webSocketServer : sendAll){
try {
webSocketServer.sendMessage(message); //调用sendMessage方法
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!--引入jquery文件-->
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
var socket; //定义了一个socket的变量
function openSocket() {
if (typeof(WebSocket) == "undefined"){ //表示一个异常
console.log("您的浏览不支持websocket"); //弹出一个日志
}else {
console.log("您的浏览器支持websocket")
//实例化websocket对象,指定要连接的服务器地址与端口,建立连接
//等同于socket = new WebSocket("ws://localhost:8080/xxxx/im/25");
//var socketUrl="${request.contextPath}/im/"+$("userId").val();
var host=window.location.host; //定义了一个host变量
socket=new WebSocket("ws://"+"localhost:8080/"+"/websocket/"+$("#userId").val())
socket.onopen=function () {
console.log("已开启")
}
socket.onclose=function () {
console.log("已关闭")
}
socket.onmessage=function (evt) {//参数数据
console.log(evt.data)
}
socket.onerror=function () {
console.log("连接异常")
}
}
}
function sendMessage() {
socket.send($("#B").val())
}
</script>
</head>
<body>
<div>请输入您的消息:<input type="text" name="B" id="B"></div>
<div>请输入您的昵称:<input type="text" name="userId" id="userId"></div>
<!-- onclick 绑定点击事件-->
<div><input type="button" value="开启websocket" onclick="openSocket()"></div>
<div><input type="button" value="发送消息" onclick="sendMessage()"></div>
</body>
</html>
当我们在处理页面数据自动更新的时候,在使用js不断的请求服务器,查看是否有新数据,如果有就获取到新数据,进行对页面信息的跟新,但是当页面长时间没有更新数据时,这样就会存在资源浪费的情况,所以才会使用WebSocket来解决。
TCP是事先为所发送的数据开辟出连接好的通道,然后再进行数据发送;而UDP则不为IP提供可靠性、流控或差错恢复功能。一般来说,TCP对应的是可靠性要求高的应用,而UDP对应的则是可靠性要求低、传输经济的应用。
WebSocket是HTML5一种新的协议,WebSocket是真正实现了全双工通信的服务器向客户端推的互联网技术,是一种在单个TCP连接上进行全双工通讯协议。
全双工是通讯传输的一个术语。通信允许数据在两个方向上同时传输,他在能力上相当于两个单工通信方式的结合。全双工指可以同时进行信号的双向传输。
全双工是:例如我们使用的手机就是全双工,在同一时刻两个用户可以同时给对方传送数据
半双工:例如我们使用的对讲机,当A方按住通话按钮才可以向B方传送数据,B方也是,在同一时刻只有一个用户能够传送数据(A/用户都可以传递信息,但是不能够同时传递)
单工:例如我们看电视时,我们只能接收对方发送的信息,不能够给对方传递信息;
Socket是应用层与TCP/IP协议通信的中间软件抽象层,它是一组接口。而WebSocket则不同,它是一个完整的应用层协议,包含一套标准的API。
http协议是短链接,因为请求之后,都会关闭连接,下次重新请求数据,需要再次打开连接。WebSocket协议是一种长连接,只需要通过一次请求来初始化链接,然后所有的请求和响应都是通过这个TCP链接进行通信。
@ServerEndpoint 类似与servlet中的 RequestMapping
@OnOpen类似与servlet中的 init()初始化
@OnClose类似与servlet中的destroy() 销毁
@OnMessage类似于servlet中的service请求 (意思就是发送数据的方式 @doPost() / @doGet() 组合)
应用层、表示层、会话层、传输层、网络层、数据传输层、物理层
三次握手(Three-way Handshake)是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。主要作用是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
第一次握手:客户端向服务端发送一个SYN报文,表示建立连接
? 得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端收到后向客户端发送,SYN,ACK报文,为这次连接分配资源
? 得出结论:服务端的发送能力正常
第三次握手:客户端收到ACK报文后,向Server端发送ACK报文,并分配资源,到这里就可以认为客户端与服务端已经建立了连接
? 得出结论:客户端的接受能力正常。至此服务端和客户端的发送和接受能力正常,
过程描述
①四川8633请求建立连接(SYN),并且发送出序号。
②服务端接受到信号,即有确认号(ACK),此时并同样返回请求序号Seq
③客户端接受到信号,即有确认号(ACK),连接已经建立。
SYN:表示建立连接
FIN:表示释放连接
ACK:确认序号有效。表示响应
CLOSE_WAIT:被动关闭
LAST_ACK:等待中断请求的确认状态
连接终止协议(四次挥手)
? 由于TCP连接是全双工的,因此每个方向都必须单独进行关闭,它是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN仍能发送数据。首先进行关闭的一个方法将执行主动关闭,而另一方执行被动关闭
第一次挥手:客户端向服务端发送一个FIN报文,用来关闭客户到服务器的数据传送。
第二次挥手:服务器收到这个FIN,会给客户端发送一个ACK,Server进入CLOSE_WAIT状态
第三次挥手:服务器向发送一个FIN报文给客户端,用来关闭服务端到客户端的数据传送,Server进入LAST_ACK状态
第四次挥手:客户端收到FIN后,给服务端发送一个ACK报文确认,并将确认序号设置为收到序号+1,Server进入CLOSED状态,完成四次挥手
//过程描述
A:“任务处理完毕,我希望断开连接。”
B:“哦,是吗?请稍等,我准备一下。”
等待片刻后……
B:“我准备好了,可以断开连接了。”
A:“好的,关闭连接”
川航图举例
①客户端申请断开连接即FIN,发送Seq+Ack
②服务端接收信息返回,表示我已经接收到
③服务端端发送信息表示可以断开连接
④客户端接受信息,返回数据表示已接受信息