Java_SpringBoot+WebSocket长连接_独乐乐不如众乐乐_南国_spring websocket连接时间配置-程序员宅基地

技术标签: Java  java  websocket  http  springboot  

Java使用WebSocket

网页端的消息推送,一般有以下几种方式

轮询方式:

客户端定时向服务端发送ajax请求,服务器接收到请求后马上返回消息并关闭连接。
优点:后端程序编写比较容易。
缺点:TCP的建立和关闭操作浪费时间和带宽,请求中有大半是无用,浪费带宽和服务器资源。

实例:适于小型应用。

长轮询:

客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
优点:在无消息的情况下不会频繁的请求,耗费资源小。
缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。
实例:WebQQ、Hi网页版、Facebook IM。

长连接:

在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据。
优点:消息即时到达,不发无用请求;管理起来也相对方便。
缺点:服务器维护一个长连接会增加开销,当客户端越来越多的时候,server压力大!
实例:Gmail聊天

Flash Socket:

在页面中内嵌入一个使用了Socket类的 Flash 程序JavaScript通过调用此Flash程序提供的Socket接口与服务器端的Socket接口进行通信,JavaScript在收到服务器端传送的信息后控制页面的显示。
优点:实现真正的即时通信,而不是伪即时。
缺点:客户端必须安装Flash插件,移动端支持不好,IOS系统中没有flash的存在;非HTTP协议,无法自动穿越防火墙。
实例:网络互动游戏。

webSocket:

HTML5 WebSocket设计出来的目的就是取代轮询和长连接,使客户端浏览器具备像C/S框架下桌面系统的即时通讯能力,实现了浏览器和服务器全双工通信,建立在TCP之上,虽然WebSocket和HTTP一样通过TCP来传输数据,但WebSocket可以主动的向对方发送或接收数据,就像Socket一样;并且WebSocket需要类似TCP的客户端和服务端通过握手连接,连接成功后才能互相通信。
优点:双向通信、事件驱动、异步、使用ws或wss协议的客户端能够真正实现意义上的推送功能。
缺点:少部分浏览器不支持。
示例:社交聊天(微信、QQ)、弹幕、多玩家玩游戏、协同编辑、股票基金实时报价、体育实况更新、视频会议/聊天、基于位置的应用、在线教育、智能家居等高实时性的场景。

综合来看,各有优缺点,不过WebSocket长连接更实用,微信,QQ,聊天室等

既然要使用Socket,就要弄懂两个东西,什么是Socket?为什么要用Socket?

什么是WebSocket?

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

为什么要用Socket?

在这里插入图片描述初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息。
举例来说,我们想要查询当前的排队情况,只能是页面轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此WebSocket 就是这样发明的。

-------------------这是一道华丽的分割线---------------------------

从网上看了很多文章感觉,很多Demo运行不了,然后就找了很多,综合了很多
无论是Tomcat版本,还是Spring拦截器版本,配置都好多,不好集成,对比之后还是SpringBoot比较好用

废话不多说,直接上代码,跟着我做,肯定可以运行起来的

第一步:引包,加入maven依赖

<dependency>  
           <groupId>org.springframework.boot</groupId>  
           <artifactId>spring-boot-starter-websocket</artifactId>  
</dependency> 

第二步:启用WebSocket

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * 开启WebSocket支持
 * @author zhangzhiwei
 */
@Configuration  
public class WebSocketConfig {
      
	
    @Bean  
    public ServerEndpointExporter serverEndpointExporter() {
      
        return new ServerEndpointExporter();  
    }  
  
} 

第三部:编码WebSocket服务端

package com.hnyfkj.argotechnique.web.controller;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
/**
 * WebSocket服务
 * @author zhangzhiwei
 */
@ServerEndpoint("/websocket/{userId}")
@Component
public class WebsocketControler {
    

	static Log log = LogFactory.get(WebsocketControler.class);
	private static int onlineCount = 0;
	// concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
	private static ConcurrentHashMap<String, WebsocketControler> clients = new ConcurrentHashMap<>();
	private Session session;
	private String userId;

	/**
	 * 连接建立成功调用的方法
	 *
	 * @param session
	 *            可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
	 * @throws IOException
	 */
	@OnOpen
	public void onOpen(@PathParam("userId") String userId, Session session) throws IOException {
    
		this.session = session;
		this.userId = userId;
		if (clients.containsKey(userId)) {
    
			clients.remove(userId);
			clients.put(userId, this);
			// 加入set中
		} else {
    
			clients.put(userId, this);
			// 加入set中
			addOnlineCount();
			// 在线数加1
		}

		log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount());

		try {
    
			sendMessage("连接成功");
		} catch (IOException e) {
    
			log.info("用户:" + userId + ",网络异常!!!!!!");
		}
	}

	/**
	 * 连接关闭调用的方法
	 */
	@OnClose
	public void onClose(@PathParam("userId") String userId, Session session) {
    
		if (clients.containsKey(userId)) {
    
			clients.remove(userId);
			// 从set中删除
			subOnlineCount();
		}
		log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
	}

	/**
	 * 收到客户端消息后调用的方法
	 *
	 * @param message
	 *            客户端发送过来的消息
	 * @param session
	 *            可选的参数
	 * @throws IOException
	 */
	@OnMessage
	public void onMessage(String message, Session session) throws IOException {
    
		log.info("用户消息:" + userId + ",报文:" + message);
		// 可以群发消息
		// 消息保存到数据库、redis
		if (StringUtils.isEmpty(message)) {
    
			try {
    
				// 解析发送的报文
				JSONObject jsonObject = JSON.parseObject(message);
				// 追加发送人(防止串改)
				jsonObject.put("fromUserId", this.userId);
				String toUserId = jsonObject.getString("toUserId");
				// 传送给对应toUserId用户的websocket
				if (StringUtils.isEmpty(toUserId) && clients.containsKey(toUserId)) {
    
					clients.get(toUserId).sendMessage(jsonObject.toJSONString());
				} else {
    
					log.error("请求的userId:" + toUserId + "不在该服务器上");
					// 否则不在这个服务器上,发送到mysql或者redis
				}
			} catch (Exception e) {
    
				e.printStackTrace();
			}
		}
	}

	/**
	 * 实现服务器主动推送
	 */
	public void sendMessage(String message) throws IOException {
    
		this.session.getBasicRemote().sendText(message);
	}

	/**
	 * 发生错误时调用
	 *
	 * @param session
	 * @param error
	 */
	@OnError
	public void onError(Session session, Throwable error) {
    
		log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
		error.printStackTrace();
	}

	/**
	 * 发送自定义消息
	 */
	public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
    
		log.info("发送消息到:" + userId + ",报文:" + message);
		if (!StringUtils.isEmpty(userId) && clients.containsKey(userId)) {
    
			clients.get(userId).sendMessage(message);
		} else {
    
			log.error("用户" + userId + ",不在线!");
		}
	}

	public static synchronized int getOnlineCount() {
    
		return onlineCount;
	}

	public static synchronized void addOnlineCount() {
    
		WebsocketControler.onlineCount++;
	}

	public static synchronized void subOnlineCount() {
    
		WebsocketControler.onlineCount--;
	}

	public static synchronized Map<String, WebsocketControler> getClients() {
    
		return clients;
	}
}

第四步:消息推送接口编写

@RestController
@RequestMapping("/")
@Api(tags = {
     "平台接口" })
public class MarketInterface {
    

	@Reference(version = "${dubbo.consumer.MemberinfoService.version}")
	private MemberinfoService memberinfoService;

	private static Map<String, List<String>> clients = new HashMap<String, List<String>>();

	@PostMapping("/pushMessage")
	@ApiOperation(value = "推送消息", produces = "application/json")
	public RespDto<String> pushMessage(final String userId, final String message) throws IOException {
    
		// 向指定用户集合中添加消息
		List<String> list = clients.get(userId);
		if (StringUtils.isEmpty(list)) {
    
			list = new ArrayList<>();
		}
		list.add(message);
		clients.put(userId, list);
		// 向指定用户发送消息
		for (String message1 : list) {
    
			WebsocketControler.sendInfo(message1, userId);
		}
		return RespDto.succ();
	}

	@PostMapping("/removeMessage")
	@ApiOperation(value = "消费消息", produces = "application/json")
	public RespDto<String> removeMessage(final String userId) throws IOException {
    
		// 删除消息
		clients.remove(userId);
		return RespDto.succ("消费消息成功");
	}

这里是根据我业务需求写的,你们只需要调用第一个方法就可以了,我这里需要前台告诉我收到消息了,我才可以消费消息,防止发了前台没收到这种情况,所以写了两个接口,一个发送消息,一个消费消息

第五步:Html页面,这种页面网上到处都是,随便占一个

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket示例</title>
    <meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' name='viewport' />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
    <input id="text" type="text"/>
    <button onclick="send()">发送消息</button>
    <hr/>
    <button onclick="closeWebSocket()">关闭WebSocket连接</button>
    <hr/>
    <div id="message"></div>
</body>

<script type="text/javascript">
    var websocket = null;
    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
    
        // 不带参数的写法
        websocket = new WebSocket("ws://127.0.0.1:8093/hnyfkj-argotechnique/websocket/2");
       
    }
    else {
    
        alert('当前浏览器 Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function () {
    
        setMessageInnerHTML("WebSocket连接发生错误");
    };

    //连接成功建立的回调方法
    websocket.onopen = function () {
    
        setMessageInnerHTML("WebSocket连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
    
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
    
        setMessageInnerHTML("WebSocket连接关闭");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
    
        closeWebSocket();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
    
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //关闭WebSocket连接
    function closeWebSocket() {
    
        websocket.close();
    }

    //发送消息
    function send() {
    
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
</script>
</html>

这里的那个userId是用来建立管道的,根据userId的不同,进行不同用户的信息推送,这里我写死了,用一个数字2,实际开发要拿到变量来进行后台交互

第六步:看效果,改BUG

1.运行Java代码,打开Html,出现下面红框即表示成功
在这里插入图片描述这里后台跟前台建立了连接

2.PostMan模仿发送请求
在这里插入图片描述调用接口,向前台推送消息,你好啊,前台

调用后台接口之后,后台会打印报文,以及在线连接数

3.后台控制台出现这样的打印即代表成功
在这里插入图片描述成功之后,前台就会收到这样子的消息啦,聊天室一般都是这样子做的,是不是感觉很简单,哈哈哈哈

在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Jaeger_Java/article/details/105576711

智能推荐

使用nginx解决浏览器跨域问题_nginx不停的xhr-程序员宅基地

文章浏览阅读1k次。通过使用ajax方法跨域请求是浏览器所不允许的,浏览器出于安全考虑是禁止的。警告信息如下:不过jQuery对跨域问题也有解决方案,使用jsonp的方式解决,方法如下:$.ajax({ async:false, url: 'http://www.mysite.com/demo.do', // 跨域URL ty..._nginx不停的xhr

在 Oracle 中配置 extproc 以访问 ST_Geometry-程序员宅基地

文章浏览阅读2k次。关于在 Oracle 中配置 extproc 以访问 ST_Geometry,也就是我们所说的 使用空间SQL 的方法,官方文档链接如下。http://desktop.arcgis.com/zh-cn/arcmap/latest/manage-data/gdbs-in-oracle/configure-oracle-extproc.htm其实简单总结一下,主要就分为以下几个步骤。..._extproc

Linux C++ gbk转为utf-8_linux c++ gbk->utf8-程序员宅基地

文章浏览阅读1.5w次。linux下没有上面的两个函数,需要使用函数 mbstowcs和wcstombsmbstowcs将多字节编码转换为宽字节编码wcstombs将宽字节编码转换为多字节编码这两个函数,转换过程中受到系统编码类型的影响,需要通过设置来设定转换前和转换后的编码类型。通过函数setlocale进行系统编码的设置。linux下输入命名locale -a查看系统支持的编码_linux c++ gbk->utf8

IMP-00009: 导出文件异常结束-程序员宅基地

文章浏览阅读750次。今天准备从生产库向测试库进行数据导入,结果在imp导入的时候遇到“ IMP-00009:导出文件异常结束” 错误,google一下,发现可能有如下原因导致imp的数据太大,没有写buffer和commit两个数据库字符集不同从低版本exp的dmp文件,向高版本imp导出的dmp文件出错传输dmp文件时,文件损坏解决办法:imp时指定..._imp-00009导出文件异常结束

python程序员需要深入掌握的技能_Python用数据说明程序员需要掌握的技能-程序员宅基地

文章浏览阅读143次。当下是一个大数据的时代,各个行业都离不开数据的支持。因此,网络爬虫就应运而生。网络爬虫当下最为火热的是Python,Python开发爬虫相对简单,而且功能库相当完善,力压众多开发语言。本次教程我们爬取前程无忧的招聘信息来分析Python程序员需要掌握那些编程技术。首先在谷歌浏览器打开前程无忧的首页,按F12打开浏览器的开发者工具。浏览器开发者工具是用于捕捉网站的请求信息,通过分析请求信息可以了解请..._初级python程序员能力要求

Spring @Service生成bean名称的规则(当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致)_@service beanname-程序员宅基地

文章浏览阅读7.6k次,点赞2次,收藏6次。@Service标注的bean,类名:ABDemoService查看源码后发现,原来是经过一个特殊处理:当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String C..._@service beanname

随便推点

二叉树的各种创建方法_二叉树的建立-程序员宅基地

文章浏览阅读6.9w次,点赞73次,收藏463次。1.前序创建#include&lt;stdio.h&gt;#include&lt;string.h&gt;#include&lt;stdlib.h&gt;#include&lt;malloc.h&gt;#include&lt;iostream&gt;#include&lt;stack&gt;#include&lt;queue&gt;using namespace std;typed_二叉树的建立

解决asp.net导出excel时中文文件名乱码_asp.net utf8 导出中文字符乱码-程序员宅基地

文章浏览阅读7.1k次。在Asp.net上使用Excel导出功能,如果文件名出现中文,便会以乱码视之。 解决方法: fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);_asp.net utf8 导出中文字符乱码

笔记-编译原理-实验一-词法分析器设计_对pl/0作以下修改扩充。增加单词-程序员宅基地

文章浏览阅读2.1k次,点赞4次,收藏23次。第一次实验 词法分析实验报告设计思想词法分析的主要任务是根据文法的词汇表以及对应约定的编码进行一定的识别,找出文件中所有的合法的单词,并给出一定的信息作为最后的结果,用于后续语法分析程序的使用;本实验针对 PL/0 语言 的文法、词汇表编写一个词法分析程序,对于每个单词根据词汇表输出: (单词种类, 单词的值) 二元对。词汇表:种别编码单词符号助记符0beginb..._对pl/0作以下修改扩充。增加单词

android adb shell 权限,android adb shell权限被拒绝-程序员宅基地

文章浏览阅读773次。我在使用adb.exe时遇到了麻烦.我想使用与bash相同的adb.exe shell提示符,所以我决定更改默认的bash二进制文件(当然二进制文件是交叉编译的,一切都很完美)更改bash二进制文件遵循以下顺序> adb remount> adb push bash / system / bin /> adb shell> cd / system / bin> chm..._adb shell mv 权限

投影仪-相机标定_相机-投影仪标定-程序员宅基地

文章浏览阅读6.8k次,点赞12次,收藏125次。1. 单目相机标定引言相机标定已经研究多年,标定的算法可以分为基于摄影测量的标定和自标定。其中,应用最为广泛的还是张正友标定法。这是一种简单灵活、高鲁棒性、低成本的相机标定算法。仅需要一台相机和一块平面标定板构建相机标定系统,在标定过程中,相机拍摄多个角度下(至少两个角度,推荐10~20个角度)的标定板图像(相机和标定板都可以移动),即可对相机的内外参数进行标定。下面介绍张氏标定法(以下也这么称呼)的原理。原理相机模型和单应矩阵相机标定,就是对相机的内外参数进行计算的过程,从而得到物体到图像的投影_相机-投影仪标定

Wayland架构、渲染、硬件支持-程序员宅基地

文章浏览阅读2.2k次。文章目录Wayland 架构Wayland 渲染Wayland的 硬件支持简 述: 翻译一篇关于和 wayland 有关的技术文章, 其英文标题为Wayland Architecture .Wayland 架构若是想要更好的理解 Wayland 架构及其与 X (X11 or X Window System) 结构;一种很好的方法是将事件从输入设备就开始跟踪, 查看期间所有的屏幕上出现的变化。这就是我们现在对 X 的理解。 内核是从一个输入设备中获取一个事件,并通过 evdev 输入_wayland

推荐文章

热门文章

相关标签