java实现WebSscoket,包含踩坑日记(粘贴即用)

写在最前面,原理部分。

什么是webScoket?

websocket 是一种网络通信协议,一般用来进行实时通信会使用到,webscoket是html5开始提供的一种在TCP连接上的全双工通讯协议。

什么是全双工协议,和单工通信有什么区别?

全双工协议是指在通信过程中,双方可以同时发送和接收数据的协议。在全双工通信中,发送方和接收方可以同时进行数据传输,不需要等待对方的响应。

单工通信是指通信双方只能在一个方向上进行数据传输的协议。在单工通信中,通信双方只能在一个特定的时间窗口内进行数据传输,无法同时发送和接收数据。

说人话就是,单工通信就是只能由一方发消息,另一方接受。双全工就是双方可以没有限制的自由发送消息,建立两个通道,ab都可以使用这两个通道,两个通道,一个用于发送消息,另一个用于接收消息。这样,双方既可以发送消息给对方,也可以接收对方发送的消息。

那么http协议是如何升级为webScoket协议的呢?

客户端发送一个HTTP请求到服务器,请求升级到WebSocket协议。请求头中包含一个特殊的字段Upgrade: websocket,以及其他必要的字段如Connection: Upgrade等。

服务器接收到该请求后,进行验证和处理。如果服务器支持WebSocket协议,会返回一个HTTP 101 Switching Protocols的响应,表示协议切换成功。响应头中包含Upgrade: websocket和Connection: Upgrade字段。

客户端收到服务器的响应后,即可确认协议升级成功。此时,客户端和服务器之间的连接从HTTP协议切换到了WebSocket协议。

那么我们知道http协议要经过三次握手。所以一个webScoket请求也是三次握手成功之后,再进行请求升级的。

WebSocket和HTTP请求在数据传输上有以下不同之处?

数据格式:HTTP请求和响应的数据格式通常是文本形式,使用HTTP头和HTTP体来传输数据。WebSocket使用帧(frame)来传输数据,帧可以是文本、二进制数据或其他自定义格式。

请求方式:HTTP请求有多种请求方法,如GET、POST、PUT、DELETE等,用于不同的操作和数据传输需求。而WebSocket没有特定的请求方法,只有建立连接的握手过程,之后通过发送和接收帧来进行数据传输。

连接维持:HTTP请求每次都需要重新建立连接,每次请求和响应都是独立的。而WebSocket连接是持久的,可以保持长时间的连接,不需要重复建立。

所以webScoket是一套跟http完全不一样的通讯方式。那么实现也不一致,java已经对于webScoket进行了实现,参考下面代码实现即可。

首先对于webScoket的理解和代码,其他文章都大差不差,我也就不过多赘述。
接下来是代码的简单实现

package com.pix.webScoket.Client;

import com.alibaba.fastjson.JSONObject;
import com.pix.webScoket.webScoketEnum.MessageTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint(value = "/api/v2/ws/{info}")
@Slf4j
@Component
@Service
public class WebSocketServer {


	// 在线连接
	private static Map<String, List<Session>> onlineUsers = new ConcurrentHashMap<>();

	//当前用户信息
	private String info;

	//缓存分组,根据业务id
	private static Map<String,String> cacheGroupByPatientId = new ConcurrentHashMap<>();

	//业务信息分组,根据业务id
	private static Map<String,List<String>> userInfoGroupByPatientId = new ConcurrentHashMap<>();

	//创建连接时初始化信息
	@OnOpen
	public void onOpen(@PathParam("info") String info, Session session ,EndpointConfig config) {
		this.info = info;
		saveSessionAndInfo(info, session);
		//广播消息
		this.broadcastAllUser(this.getOnleMessage());
	}

	//断开连接时调用
	@OnClose
	public void onClose(@PathParam("info") String info,Session session, CloseReason closeReason) {
		removeSessionAndInfo(info, session);
		//通知其他用户已下线
		//广播消息
		this.broadcastAllUser(this.getClosMessage());
	}


	//收到客户端发送的消息,这里直接转发给所有人了
	@OnMessage
	public void onMessage(@PathParam("info") String info, String messageString, Session session) {
		if ("test".equals(messageString)){
			return;
		}
		Map<String,Object> message = new HashMap<>();
		message.put("code", MessageTypeEnum.TASK_LIST.getCode());
		message.put("receiver",messageString);
		String patienId = getPatienId(info);
		WebSocketServer.cacheGroupByPatientId.put(patienId,messageString);
		message.put("userSet",onlineUsers.keySet());
		this.broadcastAllUser(JSONObject.toJSONString(message));
	}

	//广播消息
	public void broadcastAllUser(String message){
		String patientId = getPatienId(info);
		try {
			List<Session> sessions = onlineUsers.get(patientId);
			for (Session session : sessions) {
				session.getBasicRemote().sendText(message);
			}
		}catch (Exception e){
			// TODO: 2023/7/26 记录日志
		}

	}

	private String getOnleMessage(){
		Map<String,Object> message = new HashMap<>();
		message.put("code",MessageTypeEnum.ITEM_WARN_LIST.getCode());
		message.put("receiver", this.info +"加入文档编辑");
		String patienId = getPatienId(this.info);
		Map<String, String> cacheGroupByPatientIdR = WebSocketServer.cacheGroupByPatientId;
		message.put("onOpen", ObjectUtils.isEmpty(cacheGroupByPatientIdR)?null:cacheGroupByPatientIdR.get(patienId));
		Map<String, List<String>> userInfoR = WebSocketServer.userInfoGroupByPatientId;
		message.put("userSet", ObjectUtils.isEmpty(userInfoR)?null:userInfoR.get(patienId));
		return JSONObject.toJSONString(message);
	}

	private String getClosMessage(){
		Map<String,Object> message = new HashMap<>();
		message.put("code",MessageTypeEnum.ITEM_WARN_LIST.getCode());
		message.put("receiver",this.info +"退出文档编辑");
		String patienId = getPatienId(this.info);
		message.put("userSet",WebSocketServer.userInfoGroupByPatientId.get(patienId));
		return JSONObject.toJSONString(message);
	}

	private static void saveSessionAndInfo(String info, Session session) {
		//session进行保存
		String patientId = getPatienId(info);
		List<Session> sessions = WebSocketServer.onlineUsers.get(patientId);
		if (ObjectUtils.isEmpty(sessions)){
			sessions = new ArrayList<>();
		}
		sessions.add(session);
		WebSocketServer.onlineUsers.put(patientId,sessions);
		//info保存
		List<String> infoList =  WebSocketServer.userInfoGroupByPatientId.get(patientId);
		if (ObjectUtils.isEmpty(infoList)){
			infoList = new ArrayList<>();
		}
		infoList.add(info);
		WebSocketServer.userInfoGroupByPatientId.put(patientId,infoList);
	}

	private static void removeSessionAndInfo(String info, Session session) {
		///session进行移除
		String patientId = getPatienId(info);
		List<Session> sessions = onlineUsers.get(patientId);
		if (ObjectUtils.isEmpty(sessions)){
			sessions = new ArrayList<>();
		}
		sessions.remove(session);
		onlineUsers.put(patientId,sessions);
		//info移除
		List<String> infoList = userInfoGroupByPatientId.get(patientId);
		if (ObjectUtils.isEmpty(infoList)){
			infoList = new ArrayList<>();
		}
		infoList.remove(info);
		WebSocketServer.userInfoGroupByPatientId.put(patientId,infoList);
	}

	private static String getPatienId(String info) {
		String[] split = info.split("\\|");
		String patientId = split[2];
		return patientId;
	}



}

配置类的简单实现

package com.pix.webScoket.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;

@Configuration
@EnableWebSocket
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

Scoket枚举

package com.pix.webScoket.webScoketEnum;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 腕表WebSocket
 * 客户端和服务端交互的消息类型
 * @author: liyadong
 */
@Getter
@AllArgsConstructor
public enum MessageTypeEnum {
    TASK_LIST(0, "数据转发"),
    ITEM_WARN_LIST(1, "上下线通知"),
    TASK_WARN_LIST(2, "心跳"),
    ;

    private int code;

    private String des;

    /**
     * 由String 转换为枚举值
     *
     * @param des
     * @return
     */
    public static MessageTypeEnum getFrom(String des) {
        for (MessageTypeEnum tmp : MessageTypeEnum.values()) {
            if (des.equals(tmp.getDes())) {
                return tmp;
            }
        }
        return null;
    }
}

踩坑1:
webScoket中只能更改一个请求头,Sec-WebSocket-Protocol。如果这个作为token携带的访视,那么就一定要再次放到这个请求头中返回去,每一次都需要。
我的实现,因为我使用的是spring securty来进行解决的,所以在过滤器进行了token的处理,改为首先从cookie中获取,下面是我的过滤器实现。那么就绕过了Sec-WebSocket-Protocol这个请求头,前端不要往这个请求头塞任何值,塞了值但是么有原封不动的返回去,那么就会报错。

package com.yaoteng.paltform.system.security;


import com.yaoteng.paltform.enums.ErrorCode;
import com.yaoteng.paltform.system.config.SecurityIgnoreUrl;
import com.yaoteng.paltform.system.exception.ServiceException;
import com.yaoteng.paltform.system.utils.JwtUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.stream.Stream;

/**
 * 定义请求过滤器,在请求来到的时候完成一次过滤,但是这里并不能被全局异常处理捕获到,并且返回给前端,所以处理的实现是再包一层,详情 ExceptionHandlerFilter
 */
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    public static final String AUTHORIZATION = "Authorization";
    public static final String SecWebSocketProtocol = "Sec-WebSocket-Protocol";
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private SecurityIgnoreUrl securityIgnoreUrl;



    @Override
    @Transactional(rollbackFor = Exception.class)
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader(AUTHORIZATION);
        String secWebSocketProtocol = request.getHeader(SecWebSocketProtocol);
        log.info(request.getRequestURL().toString());
        Stream<RequestMatcher> matchers = Arrays.stream(securityIgnoreUrl.getUrls()).map(AntPathRequestMatcher::new);
        if (matchers.anyMatch(matcher -> matcher.matches(request))) {
            filterChain.doFilter(request, response);
            return;
        }
        if (secWebSocketProtocol!=null){
            token = secWebSocketProtocol;
        }
        if (token == null) {
            Cookie[] cookies = request.getCookies();
            Cookie cookie = null;
            for (int i = 0; i < cookies.length; i++) {
                Cookie cookie1 = cookies[i];
                if ("Admin-Token".equals(cookie1.getName())){
                    cookie = cookie1;
                }
            }
            token = cookie.getValue();
        }
        if (token == null) {
            throw new ServiceException(ErrorCode.TOKEN_IS_EMPTY);
        }

        if (JwtUtil.verify(token) && JwtUtil.isExpired(token)) {
            throw new ServiceException(ErrorCode.TOKEN_IS_EXPIRED);
        }

        if (!JwtUtil.verify(token)) {
            throw new ServiceException(ErrorCode.TOKEN_IS_ILLEGAL);
        }

        String username = JwtUtil.getUsername(token);
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);

    }
}

踩坑二:
确保你的Nginx版本支持WebSocket。WebSocket在Nginx 1.3版本及以上才被支持。

在Nginx的配置文件中,添加以下代码块来启用WebSocket代理:

location /websocket {
    proxy_pass http://your_backend_server;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

在上面的代码中,将your_backend_server替换为实际的后端服务器地址和端口。

保存并重新加载Nginx配置。

确保后端服务器支持WebSocket协议,并且在代码中正确处理WebSocket连接。

为了方便大家,我在这里展示我的配置文件地址
其中代码配置文件:

server:
  servlet:
    context-path: /prod-api/service
  port: 8979

因为上面有,我就展示一部分了

其中webScoket客户端
@ServerEndpoint(value = "/api/v2/ws/{info}")
@Slf4j
@Component
@Service
public class WebSocketServer {


	// 在线连接
	private static Map<String, List<Session>> onlineUsers = new ConcurrentHashMap<>();

	//当前用户信息
	private String info;

那么我的配置文件,由上面的应该改成:

location /prod-api/service/api/v2/ {
                proxy_pass http://localhost:8979;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
        }

好了,我下午要去出摊了
在这里插入图片描述

更新:因为有兄弟想要后面工具类的代码,在此更新

package com.yaoteng.paltform.enums;

package com.yaoteng.paltform.enums;

/**
 * 错误代码枚举类
 */
public enum ErrorCode {
    /**
     * 40101: token不能为空
     */
    TOKEN_IS_EMPTY(40101, "token不能为空"),
    /**
     * 40102: token已过期
     */
    TOKEN_IS_EXPIRED(40102, "token已过期"),
    /**
     * 40103: token不合法
     */
    TOKEN_IS_ILLEGAL(40103, "token不合法"),
    ;

    private final int code;
    private final String message;

    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

此类主要解析忽略路径,需要在配置文件中进行维护

package com.yaoteng.paltform.system.config;


import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "security.ignore")
@Component
public class SecurityIgnoreUrl {
    /**
     * url集合
     */
    private String[] urls;

    public String[] getUrls() {
        return urls;
    }

    public void setUrls(String[] urls) {
        this.urls = urls;
    }
}

配置文件如下:

# 放行的url
security:
  ignore:
    urls:
      - /api/v1/user/login
      - /api/v1/user/**
      - /swagger-ui.html
      - /swagger-resources/**
      - /doc.html
      - /v2/api-docs
      - /webjars/**
      - /druid/**
      - /favicon.ico

package com.yaoteng.paltform.system.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;

/**
 * Jwt工具类(认证加密解密工具类,谨慎修改)
 * 这个也是随便拷贝的,用生成和解密就完事了
 */
public final class JwtUtil {
    public static final String SECRET = "ADF!@#DF#3";

    private JwtUtil() {
    }

    public static String generateToken(Long userId, String username) {
        return JWT.create()
                .withClaim("userId", userId)
                .withClaim("username", username)
                .withExpiresAt(Instant.now().plus(1, ChronoUnit.DAYS))
                .sign(Algorithm.HMAC256(SECRET));
    }

    public static String getUsername(String token) {
        return JWT.decode(token).getClaim("username").asString();
    }

    public static Long getUserId(String token) {
        return JWT.decode(token).getClaim("userId").asLong();
    }

    public static boolean verify(String token) {
        try {
            JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
            return true;
        } catch (JWTVerificationException | IllegalArgumentException e) {
            return false;
        }
    }

    public static boolean isExpired(String token) {
        return JWT.decode(token).getExpiresAt().before(new Date());
    }
}

package com.yaoteng.paltform.system.exception;


import com.yaoteng.paltform.enums.ErrorCode;

public class ServiceException extends RuntimeException {

    private int code = 500;

    public ServiceException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
    }

    public ServiceException() {
    }

    public ServiceException(String message) {
        super(message);
    }

    public ServiceException(String message, Throwable cause) {
        super(message, cause);
    }

    public ServiceException(Throwable cause) {
        super(cause);
    }

    public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

    public int getCode() {
        return code;
    }
}

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
这里提供一个简单的实现思路: 1. 创建一个窗口,窗口上放置一个文本框组件用于输入和显示文本内容。 2. 在窗口上添加菜单栏,菜单栏中包含“复制”、“粘贴”等选项。 3. 实现“复制”功能:当用户选择“复制”选项时,获取当前文本框中选定的文本,并将其复制到剪贴板中。 4. 实现粘贴”功能:当用户选择“粘贴”选项时,从剪贴板中获取文本,并将其插入到文本框中光标所在位置。 以下是代码实现: ```java import java.awt.BorderLayout; import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JTextArea; import javax.swing.border.EmptyBorder; public class MyNotepad extends JFrame { private JPanel contentPane; private JTextArea textArea; private Clipboard clipboard; /** * Launch the application. */ public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { try { MyNotepad frame = new MyNotepad(); frame.setVisible(true); } catch (Exception e) { e.printStackTrace(); } } }); } /** * Create the frame. */ public MyNotepad() { setTitle("My Notepad"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setBounds(100, 100, 450, 300); // 创建菜单栏 JMenuBar menuBar = new JMenuBar(); setJMenuBar(menuBar); // 创建“编辑”菜单 JMenu editMenu = new JMenu("编辑"); menuBar.add(editMenu); // 创建“复制”选项 JMenuItem copyMenuItem = new JMenuItem("复制"); copyMenuItem.addActionListener(e -> copy()); editMenu.add(copyMenuItem); // 创建“粘贴”选项 JMenuItem pasteMenuItem = new JMenuItem("粘贴"); pasteMenuItem.addActionListener(e -> paste()); editMenu.add(pasteMenuItem); // 创建主面板 contentPane = new JPanel(); contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); contentPane.setLayout(new BorderLayout(0, 0)); setContentPane(contentPane); // 创建文本框 textArea = new JTextArea(); contentPane.add(textArea, BorderLayout.CENTER); // 创建剪贴板 clipboard = getToolkit().getSystemClipboard(); } // 复制功能 private void copy() { String selectedText = textArea.getSelectedText(); if (selectedText != null && !selectedText.isEmpty()) { StringSelection selection = new StringSelection(selectedText); clipboard.setContents(selection, null); } } // 粘贴功能 private void paste() { Transferable contents = clipboard.getContents(null); if (contents != null && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) { try { String text = (String) contents.getTransferData(DataFlavor.stringFlavor); int pos = textArea.getCaretPosition(); textArea.insert(text, pos); } catch (Exception e) { e.printStackTrace(); } } } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值