WebSocket 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。谈到Websocket,就不难想到Socket,几乎每一个第一次接触WebSocket的人都会内心生出一个疑问:WebSocket和Socket的区别在哪里?
Socket其实并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
当两台主机通信时,必须通过Socket连接,Socket则利用TCP/IP协议建立TCP连接。TCP连接则更依靠于底层的IP协议,IP协议的连接则依赖于链路层等更低层次。
WebSocket则是一个典型的应用层协议。
区别
Socket是传输控制层协议,WebSocket是应用层协议。
以下简要介绍一下 WebSocket 的原理及运行机制。
WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:
WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样;
WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。
非 WebSocket 模式传统 HTTP 客户端与服务器的交互如下图所示:
图 1. 传统 HTTP 请求响应客户端服务器交互图
使用 WebSocket 模式客户端与服务器的交互如下图:
图 2.WebSocket 请求响应客户端服务器交互图
上图对比可以看出,相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。
Websocket的使用相当简单,并且几乎所有浏览器都支持。首先需要在pom.xml中导入websocket的依赖文件,如下所示:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
其次需要创建一个配置类来配置socket的路径监听。
@Configuration
public class WebsocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
最后编写服务端,
@ServerEndpoint("/websocket")
@Component
@Slf4j
public class MyWebsocketServer {
/**
* 存放所有在线的客户端
*/
private static Map<String, Session> clients = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session) {
log.info("有新的客户端连接了: {}", session.getId());
//将新用户存入在线的组
clients.put(session.getId(), session);
}
/**
* 客户端关闭
* @param session session
*/
@OnClose
public void onClose(Session session) {
log.info("有用户断开了, id为:{}", session.getId());
//将掉线的用户移除在线的组里
clients.remove(session.getId());
}
/**
* 发生错误
* @param throwable e
*/
@OnError
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
/**
* 收到客户端发来消息
* @param message 消息对象
*/
@OnMessage
public void onMessage(String message) {
log.info("服务端收到客户端发来的消息: {}", message);
this.sendAll(message);
}
/**
* 群发消息
* @param message 消息内容
*/
private void sendAll(String message) {
for (Map.Entry<String, Session> sessionEntry : clients.entrySet()) {
sessionEntry.getValue().getAsyncRemote().sendText(message);
}
}
}
最后附上我最近项目实际使用的websocket服务端。
@Controller
@Slf4j
@ServerEndpoint("/websocket/{userId}/{areaId}")
public class WebSocket {
private static String liveMessageTopic = "liveMessageTopic";
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<WebSocket>();
//private static MessageService messageService;
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private String userId=null;
private String areaId=null;
//private DocSensitiveService docSensitiveService;
private static RedisUtil redisUtil= SpringUtils.getBean(RedisUtil.class);
@Autowired
private NotificationServices notificationServicesA;
private static NotificationServices notificationServices;
@Autowired
private MessageSendService messageSendServiceA;
private static MessageSendService messageSendService;
// 非Controller 直接@Autowired无法实例化,需要初始化一下
@PostConstruct
public void init() {
notificationServices = notificationServicesA;
messageSendService=messageSendServiceA;
}
/**
* 建立连接
*
* @param session
*/
@OnOpen
public void onOpen(@PathParam("userId") String userId,@PathParam("areaId") String areaId,Session session) {
for (WebSocket str : webSocketSet) {
if (str.userId.equals(userId)&&str.areaId.equals(areaId))
//判断是否为同一用户,区分用户的标识为两个一个为userId是用来做投递用户的,以及区分区域的areaId是用来区大屏区域的,只有两个都相同才判断为同一用户
//如果要重构代码建议重写equals判断这样整体效率会得到飞速提示
try {
this.session = str.session;
Message message = new Message();
message.setSendUser("-2");//-2代表服务器
message.setContent("有其他人登录了账号你已经被强制下线");
Gson gson = new Gson();
String s = gson.toJson(message);
str.sendMessage(s);
webSocketSet.remove(str); //从set中删除
str.session.close();//关闭session连接
} catch (IOException e) {
// TODO Auto-generated catch block
log.error("websocket 重复登录异常");
}
}
//新的连接建立
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
log.info("有新窗口开始监听:" + userId + ",当前在线人数为" + getOnlineCount());
this.userId = userId;
this.areaId = areaId;//初始化两个标识
try {
Message message = new Message();
message.setSendUser("-2");//-2代表服务器
message.setContent("ws连接成功,你好用户" + String.valueOf(userId));
Gson gson = new Gson();
String str = gson.toJson(message);
sendMessage(str);
//发送消息给用户证明连接成功
message.setContent("ws连接成功,你好用户" + areaId);
str = gson.toJson(message);
sendMessage(str);
//发送消息给用户证明连接成功
//发送未接收到的消息
List<MessageSend> messageSendList=messageSendService.getMessageSendListByUser(userId);
if(messageSendList.size()>0){
// 发送未接受的消息
try {
for (MessageSend messageSend : messageSendList) {
sendMessage(messageSend.getContent());
}
// 删除刚刚发送的消息
messageSendService.delete(messageSendList);
} catch (IOException e) {
log.error("websocket 历史记录异常 history getting erroy");
}
}
} catch (IOException e) {
log.error("websocket 登录消息异常");
}
List<Message> message2= notificationServices.getMessage(userId);
//查找当前是是否有历史消息
if(message2!=null) {
try {
Gson gson = new Gson();
for(int i=0;i<message2.size();i++) {
Message message=message2.get(i);
String content=gson.toJson(message);
content=content.replace("\\","");
content=content.replace("\"{","{");
content=content.replace("}\"","}");
sendMessage(content);
}
} catch (IOException e) {
log.error("websocket 历史记录异常 history getting erroy");
}
notificationServices.delMessage(userId);
}
}
@OnError
public void onError(javax.websocket.Session session, Throwable error) {
log.error("服务端发生了错误"+error);
error.printStackTrace();
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到来自窗口"+userId+"的信息:"+message);
//群发消息
for (WebSocket item : webSocketSet) {
try {
if(item.userId.equals(userId))
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* @Method 推送消息到指定userId处
* @Author QuanJiaXing
* @Version 1.0
* @Description
* @Return
* @Exception
* @Date 2020/7/7 22:16
*/
public static int sendInfo(String message,@PathParam("userId") String userId) throws IOException {
log.info("推送消息到窗口"+userId+",推送内容:"+message);
for (WebSocket item : webSocketSet) {
try {
//这里可以设定只推送给这个sid的,为0则全部推送
if(userId.equals(0)) {
item.sendMessage(message);
return 0;
}else if(item.userId.equals(userId)){
item.sendMessage(message);
return 1;
}
} catch (IOException e) {
continue;
}
}
return -1;
}
public static int sendInfoByAreaId(String message,@PathParam("areaId") String areaId) throws IOException {
int flag=0;
for (WebSocket item : webSocketSet) {
try {
//这里可以设定只推送给这个sid的,为null则全部推送
if(areaId.equals(0)) {
item.sendMessage(message);
log.info("推送消息到窗口"+areaId+",推送内容:"+message);
return flag;
}else if(item.areaId.equals(areaId)){
item.sendMessage(message);
log.info("推送消息到窗口"+areaId+",推送内容:"+message);
flag=1;
}
} catch (IOException e) {
continue;
}
}
return flag;
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
}
public static CopyOnWriteArraySet<WebSocket> getWebSocketSet() {
return webSocketSet;
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocket.onlineCount--;
}
}
实际运行效果:
图 3.服务端向客户端发送通知
图 4.客户端接收到通知
前面的关于websocket和socket的区别的原文地址如下:
https://www.php.cn/faq/467152.html
感谢大佬科普,本人本科刚毕业的小菜鸡,做此记录,大佬勿怪,溜了溜了!