Java实现WebSocket服务

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Java可以使用Tomcat提供的WebSocket库接口实现WebSocket服务,代码编写也非常的简单。现在的H5联网游戏基本上都是使用WebSocket协议,基于长连接,服务器可以主动推送消息,而不是传统的网页采用客户端轮询的方式获取服务器的消息。下面给出简单使用Tomcat的WebSocket服务的基本代码结构。

Java实现WebSocket服务


一、使用Tomcat提供的WebSocket库


   Java可以使用Tomcat提供的WebSocket库接口实现WebSocket服务,代码编写也非常的简单。现在的H5联网游戏基本上都是使用WebSocket协议,基于长连接,服务器可以主动推送消息,而不是传统的网页采用客户端轮询的方式获取服务器的消息。下面给出简单使用TomcatWebSocket服务的基本代码结构。


@ServerEndpoint("/webSocket")  
public class WebSocket {  
  @OnOpen
  public void onOpen(Session session) throws IOException{
    logger.debug("新连接");
  }
  @OnClose
  public void onClose(){
    logger.debug("连接关闭");
  }
  @OnMessage
  public void onMessage(String message, Session session) throws IOException {
    logger.debug("收到消息");
  }
  @OnError
  public void onError(Session session, Throwable error){
    error.printStackTrace();
  }
}


二、WebSocket协议的整个流程


   1. 基于TCP协议


       WebSocket本质是基于TCP协议的,采用Java编写WebSocket服务时可以使用NIO或者AIO实现高并发的服务。


   2. 握手过程


       客户端采用TCP协议连接服务器指定端口后,首先需要发送一条HTTP的握手协议


GET /web HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: 127.0.0.1:8001
Origin: http://127.0.0.1:8001
Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw==
Sec-WebSocket-Version: 13


       请求的头里面必须包含以下内容:    

 

           1. Connection 其值为Upgrade,表示升级协议


           2. Upgrade  其值为websocket,表示升级为WebSocket协议


          3. Sec-WebSocket-Key 客户端发送给服务器的密钥,用于标识每个客户端,其值是16位的随机base64编码。


           4. Sec-WebSocket-Version WebSocket的协议版本


       服务器收到这条协议验证成功后进行协议升级,并且不会关闭
Socket连接,并发送给客户端响应升级握手成功的HTTP协议包。


HTTP/1.1 101 Switching Protocols
Content-Length: 0
Upgrade: websocket
Sec-Websocket-Accept: ZEs+c+VBk8Aj01+wJGN7Y15796g=
Connection: Upgrade
Date: Wed, 21 Jun 2017 03:29:14 GMT


       响应的协议包里面,首先是101的状态码,更换协议;其中最重要的就是Sec-WebSocket-Accept字段。其值是通过客户端的Key加上固定的"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"密钥,通过采用16位的base64编码后发送给客户端验证,如果客户端也验证成功就表示握手完成。


String acc = secKey + WEBSOCK_MAGIC_TAG;
MessageDigest sh1 = MessageDigest.getInstance("SHA1");
String key = Base64.getEncoder().encodeToString(sh1.digest(acc.getBytes()));


   3. 数据的读写


       握手成功后就可以进行数据发送和读取,WebSocket的数据可以是二进制或者纯文本。每次读取和发送数据需要打包成帧数据,需要按照其标准的格式进行发送或读取才能够正常的进行数据通信。


微信图片_20220424143341.png


       上图就是帧数据的结构图,解析帧数据的代码如下,由于是摘录的部分代码,所以只能作为理解和参考,不可直接使用。


protected WebSocketFrameData ParseFrame(NetPacketBuffer bytes){
   bytes.mark();
   WebSocketFrameData frame = new WebSocketFrameData();
   int opData = bytes.readByte();
   frame.UnPackOpCodeHeader(opData); // 第一步
   int length = frame.UnPackMaskHeader(bytes.readByte()); // 第二步
   // 读取长度
   if (length == 126) {
      length = bytes.readShort();
   } else if (length == 127){
      length = (int) bytes.readInt64();
   }
   // 数据不足,进来的是半包
   if(length + 4 > bytes.remaining()){
      bytes.reset(); //
      return null;
   }
   // 读取mask if frame.mMasked 
   byte[] masks = new byte[4]; // 第三步
   for (int i = 0; i < 4; i++) {
      masks[i] = (byte) bytes.readByte();
   }
   frame.mLength = length;
   frame.mData = bytes.readMulitBytes(length);
   frame.MaskData(masks);  // 第四步
   return frame;
}


       上面代码中第一步是解析出当前帧是否是最后帧mFin标记、操作码mOpCode,采用位处理,具体的实现如下。


public void UnPackOpCodeHeader(int opData){
  mRsv1 = (opData & 64) == 64;
  mRsv2 = (opData & 32) == 32;
  mRsv3 = (opData & 16) == 16;
  mFin = (opData & 128) == 128;
  mOpCode = (opData & 15);
}


       第二步在读取长度前,先解析当前帧是否有采用Mask掩码加密处理,并且里面有可能包含整个帧的长度信息,具体看上面的判断代码。


public int UnPackMaskHeader(int mkData){
  mMasked = (mkData & 128) == 128;
  return (mkData & 127); // 这里返回的是长度信息
}


       接下来就是读取Mask内容,注意只有客户端发送给服务端时需要采用Mask对数据做处理,服务端发送给客户端时不需要做处理。最后通过Mask掩码解析出真实数据。


public void MaskData(byte[] masks){
  if (!mMasked or masks.length == 0) return ;
  for (int i = 0; i < mLength; i++) {
      mData[i] = (byte) (mData[i] ^ masks[i % 4]);
  }
}


       以上就解析出单帧的数据,帧数据可以分为消息数据(细分为文本数据和二进制数据)、PING包、PONG包、CLOSE包、CONTINUATION包(数据未发送完成包)。而且帧数据又有mFin标记数据是否完整,否则需要将多个帧数据合成一个完整的消息数据。


// 读取帧数据,可能存在多帧数据,因此需要手动拆分
WebSocketFrameData frame = ParseFrame(mCachePacket);
if(frame == null){
  break; // 说明数据不完整,暂不处理。
}
// 不完整的帧的时候,只有第一帧会标记帧的类型
opCode = opCode == -1? frame.mOpCode: opCode; 
mCacheFrame.append(frame.mData, 0, frame.mLength);
if(!frame.mFin) // 非完整的数据不处理。
{
  continue;
}
// 处理完整的数据
switch(opCode)
{
  case WebSocketFrameData.OP_TEXT:
  case WebSocketFrameData.OP_BINARY:
    mCacheFrame.flip();
    this.OnMessage(mCacheFrame, opCode);
    break;
  case WebSocketFrameData.OP_PING:
    this.OnPing(mCacheFrame);
    break;
  case WebSocketFrameData.OP_PONG:
    this.OnPong(mCacheFrame);
    break;
  case WebSocketFrameData.OP_CLOSE:
    this.OnClosed();
    break;
  case WebSocketFrameData.OP_CONTINUATION:
    this.Close();
    break;
}
opCode = -1;
mCacheFrame.clear();


       读取整个客户端的协议数据流程就已经完成了,服务端发送回去的数据就只需要注意两点:


       1. 大的数据包需要分帧数据发送。


       2. 不需要采用Mask掩码加密,因此Mask位置设置为0,并且不写入掩码数据。


三、最后


   WebSocket协议已经在H5的游戏中使用了,学习有助于以后工作中的使用。欢迎微信搜索"游戏测试开发"关注一起沟通交流。

相关文章
|
2月前
|
Java Maven Windows
使用Java创建集成JACOB的HTTP服务
本文介绍了如何在Java中创建一个集成JACOB的HTTP服务,使Java应用能够调用Windows的COM组件。文章详细讲解了环境配置、动态加载JACOB DLL、创建HTTP服务器、实现IP白名单及处理HTTP请求的具体步骤,帮助读者实现Java应用与Windows系统的交互。作者拥有23年编程经验,文章来源于稀土掘金。著作权归作者所有,商业转载需授权。
使用Java创建集成JACOB的HTTP服务
|
1月前
|
Web App开发 消息中间件 监控
使用 Java + WebSocket 实现简单实时双人协同 pk 答题:技术干货分享
【10月更文挑战第4天】在现代互联网应用中,实时互动已经成为提升用户体验的重要一环。特别是在在线教育、游戏竞技等领域,实时协同功能显得尤为重要。今天,我们将围绕“使用 Java + WebSocket 实现简单实时双人协同 pk 答题”这一主题,分享相关技术干货,帮助你在工作和学习中更好地理解和应用这一技术。
60 2
|
1月前
|
前端开发 Java API
JAVA Web 服务及底层框架原理
【10月更文挑战第1天】Java Web 服务是基于 Java 编程语言用于开发分布式网络应用程序的一种技术。它通常运行在 Web 服务器上,并通过 HTTP 协议与客户端进行通信。
25 1
|
1月前
|
Java 关系型数据库 MySQL
java控制Windows进程,服务管理器项目
本文介绍了如何使用Java的`Runtime`和`Process`类来控制Windows进程,包括执行命令、读取进程输出和错误流以及等待进程完成,并提供了一个简单的服务管理器项目示例。
36 1
|
1月前
|
Java 数据库
基于java的汽车服务管理系统(Car Service Management System)
基于java的汽车服务管理系统(Car Service Management System)
20 0
|
2月前
|
JSON Java 数据格式
java调用服务报错400
java调用服务报错400
59 2
|
2月前
|
JSON Java 数据格式
java调用服务报错415 Content type ‘application/octet-stream‘ not supported
java调用服务报错415 Content type ‘application/octet-stream‘ not supported
87 1
|
2月前
|
Java 数据库连接 数据库
Java服务提供接口(SPI)的设计与应用剖析
Java SPI提供了一种优雅的服务扩展和动态加载机制,使得Java应用程序可以轻松地扩展功能和替换组件。通过合理的设计与应用,SPI可以大大增强Java应用的灵活性和可扩展性。
70 18
|
3月前
|
小程序 JavaScript Java
【Java】服务CPU占用率100%,教你用jstack排查定位
本文详细讲解如何使用jstack排查定位CPU高占用问题。首先介绍jstack的基本概念:它是诊断Java应用程序线程问题的工具,能生成线程堆栈快照,帮助找出程序中的瓶颈。接着,文章通过具体步骤演示如何使用`top`命令找到高CPU占用的Java进程及线程,再结合`jstack`命令获取堆栈信息并进行分析,最终定位问题代码。
303 1
【Java】服务CPU占用率100%,教你用jstack排查定位
|
3月前
|
Java 开发者
Java SPI机制大揭秘:动态加载服务提供者,一文让你彻底解锁!
【8月更文挑战第25天】Java SPI(服务提供者接口)是一种强大的扩展机制,允许程序在运行时动态加载服务实现。本文首先介绍SPI的基本原理——定义接口并通过配置文件指定其实现类,随后通过示例演示其实现过程。接着,对比分析了SPI与反射及插件机制的不同之处,强调SPI在灵活性与扩展性方面的优势。最后,基于不同场景推荐合适的选择策略,帮助读者深入理解并有效利用SPI机制。
103 1