文章目录

  • 前言
  • webSocket 是什么
    • 与http对比
    • webSocket请求头的实例
      • webSocket 的url格式
  • WebSocketHandler
    • 我的项目实现的WebSocketHandler实例
  • HandshakeInterceptor
    • 我的项目实现HandshakeInterceptor
  • WebSocketConfigurer
  • 后端的其他配置
  • arkui的webScoket
  • 运行结果


前言

webSocket 是什么

WebSocket 是一种基于 TCP 的网络协议,支持全双工通信,允许客户端和服务器在单个长连接上实时交换数据。相比传统 HTTP 的请求-响应模式,WebSocket 更适合实时应用(如聊天、游戏、股票行情推送等)。

在webScoket中发送消息都是通过session 来进行的sessionid是我们用来区分不同浏览器以及用户的根据,本篇文章有点长,不过比较简单易懂

与http对比

特性 http webSocket
协议类型 请求-响应模型 全双工通信
连接方式 短连接(默认) 长连接
通信方向 单向(客户端发起) 双向
头部开销 每次请求都有完整头部 初次握手后头部很小
实时性 低(需要轮询) 高(实时推送)

webSocket请求头的实例

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

webSocket 的url格式

ws:// 或 wss:// (加密) + host + path + query
使用 wss://:通过 TLS 加密传输数据,避免中间人攻击。

WebSocketHandler

这个是传统的springWebSoket实现消息处理的接口,还有基于javaEE 形式的Socket 以及 webSocket+Stomp 这种跟现代化的方式

这里我只用传统的方式,下面是webSocketHandler接口的方法

public interface WebSocketHandler {

//在进行连接时触发的方法
	void afterConnectionEstablished(WebSocketSession session) throws Exception;

//处理接收的消息以及主动发送消息
	void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;

//发生错误时执行的方法
	void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;

//断开连接时执行的方法
	void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;

//消息分片
	boolean supportsPartialMessages();

}

我的项目实现的WebSocketHandler实例

!注意:webSocketHandler 与 javaEE版的Socket处理方式不一样,javaEE 默认是多例模式,而我们的传统的webSocketHandler 就不一样了它默认是单例模式,就算是主动设置成多例模式,也还是单例模式,这让我们想跟多列模式的广播方式有很大的不同,下面我的代码中有解释

package com.example.demo.socket;

@Component
//webSocketHandler默认就是单例模式与javaEE不一样是多列模式
public class tranformHander implements WebSocketHandler {

    private static final Map<String, WebSocketSession> map = new HashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    //下面拿到我在HandshakeInterceptor 中通过attributes设置的值
        Map<String, Object> attributes = session.getAttributes();
        String user = (String) attributes.get("user");
        System.out.println(user);
        map.put(user, session);

        session.sendMessage(new TextMessage("连接成功了"));
        session.sendMessage(new TextMessage("我的连接频道是" + user));
        broadcast(user);
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        String mess = (String) message.getPayload();
        System.out.println(mess);
        //进行json字符转java对象
        User user = new ObjectMapper().readValue(mess, User.class);
        System.out.println(user.getFromUser());
        if (map.get(user.getFromUser()) != null) {
            WebSocketSession fromsession = map.get(user.getFromUser());
            fromsession.sendMessage(new TextMessage(user.getMessage()));
        } else {
            session.sendMessage(new TextMessage("你输入的对象有误"));
        }
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        // 用 session 获取 user
        Map<String, Object> attributes = session.getAttributes();
        String user = (String) attributes.get("user");
        map.remove(user);
    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }
```下面是我自定义的广播的方法,开始说到了webSocket都是通过session来进行
```消息发送的
    private void broadcast(String user) {
        for (WebSocketSession session : map.values()) {
            try {
                session.sendMessage(new TextMessage(user + "我上线了"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

我的逻辑是通过token 来判断与我进行连接的是哪一个用户,然后我的map中存放的是我的用户以及对应的session 这样我就能通过token来判断我是那一个用户以及我要放送给那一个用户消息

HandshakeInterceptor

我的理解这个接口被用来对webScoket的连接进行拦截处理一些 认证方面的工作,在我看来与springMvc的拦截器 一样

下面还看一下这个接口的abstract方法

public interface HandshakeInterceptor {


	 //在握手前处理的方法
	boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
			WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception;

//在握手后处理的方法
	void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
			WebSocketHandler wsHandler, @Nullable Exception exception);

}

我的项目实现HandshakeInterceptor

这里我只重写了beforehandshake方法
attributes 加入的建值可以在WebSocketHandler 通过session来拿到 我上面的代码中有实例

package com.example.demo.config;

import java.util.Map;


@Component
public class webSocketInterceptor implements HandshakeInterceptor {
	@Autowired
	jwtUtils jwt;
	@Override
	public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
			Map<String, Object> attributes) throws Exception {
		// 例如:从 header 获取 token
		String query = request.getURI().getQuery();
		System.out.println(query);
		int size = query.length();
		String token=(String) query.subSequence(6, size);		// 校验 token(可用 JWT、数据库等方式)
		if (token != null && jwt.validateToken(token)) {
			System.out.println("执行到了认证阶段,并且认证通过");
			// 认证通过
			//在WebSocketHandler 中可以使用session来拿到attributes
			attributes.put("user",jwt.getUsernameFromToken(token));
			return true;
		}
		// 认证失败
		System.out.println("false");
		response.setStatusCode(HttpStatus.UNAUTHORIZED);
		return false;
	}

	@Override
	public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
			Exception exception) {
		// 可选:握手后处理
	}
}

WebSocketConfigurer

springMvc 有它的WebMvcConfigurer 来处理一些cors配置,拦截器配置等
WebSocketConfigurer就是与webMvcConfigurer相似进行配置的

下面我的项目的实现webSocketConfigurer的实例

还有一定要在配置类上添加上@EnableWebsocket来开启webSocket

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer,WebMvcConfigurer {
	@Autowired
	tranformHander hander;
	
	 @Autowired
	  tokenInterceptor tokenInterceptor;
	 @Autowired
	 webSocketInterceptor webInterceptor;
	    @Override
	    public void addInterceptors(InterceptorRegistry registry) {
	        registry.addInterceptor(tokenInterceptor)
	            .addPathPatterns("/**") // 拦截所有请求
	            .excludePathPatterns("/login", "/register","/book/list","/book/number","/datail");
	       
	// 放行登录、注册等接口
	    }
	    @Override
	    public void addCorsMappings(CorsRegistry registry) {
	                registry.addMapping("/**")
	                        .allowedOriginPatterns("http://localhost:56472") // 或指定你的前端地址
	                        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
	                        .allowedHeaders("*")
	                        .allowCredentials(true)
	                        .maxAge(3600);}
	    
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		// TODO Auto-generated method stub
		registry.addHandler(hander,"/ws")
		.setAllowedOrigins("*")
		.addInterceptors(webInterceptor);
		
	}

因为是前后端分离项目所以cos配置是必不可少的,这里只需要关注 registerWebSocketHandlers方法即可
可以看到我们通过addHandler方法添加上了我在上文写的WebSocketHandler
使用SetAllowedOrigins("*")来设置cors跨域问题
使用addInterceptors()来添加我们的HandshakeInterceptor

后端的其他配置

到这里我们的webSocket 方面的配置就算是完成了,因为我们使用arkui 连接起来(前后端分离)token是必不可少的,添加我们的jwtUtils,数据库的配置我这里就不书写了,如果想了解数据库的可以看一下我的其他文章

下面是jwtUtils.class

package com.example.demo.utils;
import io.jsonwebtoken.*;

import org.springframework.stereotype.Component;
import java.util.Base64;
import java.util.Date;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;

@Component
public class jwtUtils {
   
    private String secret="这里发通过key生成的密钥"
;
    private final long EXPIRATION = 86400000; // 1天

    private SecretKey getSecretKey() {
        return Keys.hmacShaKeyFor(Base64.getDecoder().decode(secret));
    }

    // 生成token
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(getSecretKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    // 解析token
    public String getUsernameFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSecretKey())
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    // 校验token
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            // 可加日志
            return false;
        }
    }
}

下面是jwt密钥生成类,直接执行就行然后天加到.yml 或直接加到jwtUtils中,这里建议放到.yml文件中然后通过@Vaule()来拿取

public class key {
    public static void main(String[] args) {
        String key = Base64.getEncoder().encodeToString(Keys.secretKeyFor(SignatureAlgorithm.HS256).getEncoded());
        System.out.println(key);
    }
}

arkui的webScoket

arkui并不支持stomp 开源包也非常的少,像webSocket 只能使用最传统的方式来进行,写起来非常的吃力
在我看来arkui与vue是非常的相似的都是MVVM模式,只要学会了vue理解了什么是单页面,理解起arkui是非常有帮助的

下面是我编写的webScoket

import { BusinessError } from '@kit.BasicServicesKit';
import { JSON } from '@kit.ArkTS';

 // 替换为实际地址
let ws = webSocket.createWebSocket();
let webSocketDemoInstance: WebSocketDemo|null
class DividerTmp {
  strokeWidth: Length = 1;
  startMargin: Length = 60;
  endMargin: Length = 10;
  color: ResourceColor = '#ffe9f0f0';
}
// 事件监听器
ws.on('open', (err: BusinessError, value: Object) => {
  if (!err) {
    console.log("连接成功打开");

  } else {
    console.error("打开连接错误:", JSON.stringify(err));

  }
});

ws.on('message', (err: BusinessError, data: String | ArrayBuffer) => {
  if (!err) {
    webSocketDemoInstance?.insertText.push(`${data}`)
  } else {

    webSocketDemoInstance?.insertText.push("接收消息错误:",`${JSON.stringify(err)}`)
  }
});


ws.on('error', (err: BusinessError) => {
  console.error("发生错误:", JSON.stringify(err));

});

// 建立连接
function connect(ip: string, callback: (success: boolean,Text:String) => void) {
  ws.connect(ip, (error: BusinessError, value: boolean) => {
    if (error) {

      callback(false, `连接操作失败: ${JSON.stringify(error)}`);
    } else {

      callback(value, `连接操作完成,结果: ${value}`);

    }
  });
}

// 关闭连接
function close(callback:(success:boolean,text:String)=>void) {
ws.close((err: BusinessError) => {
  if (!err) {

    callback(false,"close success")
  } else {

    callback(false,`close fail err is ${JSON.stringify(err)}`)
  }})}


@Entry
@Component
struct WebSocketDemo {
  @State token :String|undefined =AppStorage.get("token") ;
  @State ip: string = `ws://127.0.0.1:5454/ws?token=${this.token}`;
  @State isConnected: boolean = false;
  @State text:String='';
  @State insertText:String[]=[];
  @State egDivider: DividerTmp = new DividerTmp();
  aboutToAppear() {
    webSocketDemoInstance = this;
  }
  aboutToDisappear() {
    webSocketDemoInstance = null;
  }


  build() {
    Column() {
      Row() {
        TextInput({ placeholder: "请输入你的需要的连接" }).width("50%")
          .onChange((value: string) => {
            this.ip = value;
          })
        if (!this.isConnected) {
          Button("连接")
            .onClick(() => {
              connect(this.ip, (success, text) => {
                console.log(`${this.token}`)
                this.isConnected = success;
                this.insertText.push(text)
              });
            })
        } else {
          Button("断开")
            .onClick(() => {
              close((success, text) => {
                this.isConnected = success
                this.insertText.push(text)
              })

            })
        }
      }

      List() {
        ForEach(this.insertText, (item: String) => {
          ListItem() {
            Text(`${item}`)
          }
        })
      }.divider(this.egDivider).height(300).width('100%').scrollBar(BarState.Auto)

      if (this.isConnected) {
        TextArea({ text: $$this.text, placeholder: "这里的文本是接收与服务器交流的文本" })
          .onChange((value: String) => {
            this.text = value

          })
        Button('发送').onClick(async () => {
          ws.send(JSON.stringify({ fromUser: 'jiang',message:this.text}))
          this.insertText.push(`我:${this.text}`);
          this.text=''
        })


      }

    }
  }
}

通过 let ws = webSocket.createWebSocket();来构建webSocket实例
具体的一些方法我不过的的解释了,具体可以看官方文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/websocket-connection

运行结果

我使用postman 与 鸿蒙模拟器来进行测试

下面是我的模拟器的截图可以看到在我上线是会进行广播告诉在线的用户我伤上线了

在这里插入图片描述

下面是postman 的测试用例
在这里插入图片描述

可以看到我上线了会进行广播通知在线的用户我上线了,我使用了传统的webSocket方式写后端,以及arkui中传统的方式来书写前端webSocket来进行连接,我的例子比较简单,推荐大家使用webScoket+stomp的方式来写后端,可以进行更细致化的操作

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐