WebSocket基础知识

WebSocket的生命周期

Java Websocket API中的WebSocket生命周期

WebSocket端点的四个生命周期事件
+ 打开事件:此事件发生在端点上建立新连接时并且在任何其他事件发生之前
+ 消息事件:此事件接收WebSocket对话中另一端发送的消息。它可以发生在WebSocket端点接收了打开了事件之后并且在接收关闭事件关闭连接之前的任意时刻。
+ 错误事件:此事件在WebSocket连接或者端点发生错误时产生
+ 关闭事件:此事件表示WebSocket端点的连接目前正在部分地关闭,它可以由参与连接的任意一个端点发出

注解式端点事件处理

服务器端点需要使用一个类级别注解@ServerEndpoint,客户端端点则需要@ClientEndpoint注解。对于注解式端点来说,为了拦截不同的生命周期事件,我们需要利用方法级注解:@OnOpen,@OnMessage,@OnError和@OnClose。

  • @OnOpen 此注解用于注解式端点的方法,指示当此端点建立新的连接时调用此方法。需要一个方法来处理打开事件的主要原因是,使得开发人员能够设置在WebScoket对话中的信息,你可能希望为准备数据库而执行一些花费昂贵的必要操作,例如在处理事件的方法中打开数据库连接。此事件伴随着三部分信息:WebSocket Session对象,用于表示已经建立好的连接;配置对象(EndpointConfig的实例),包含了用来配置端点的信息;一组路径参数,用于打开阶段握手时WebSocket端点匹配入URL。
@OnOpen
public void init(Session session, EndpointConfig config){
    // initialization code
}
  • @OnMessage 此注解允许你装饰你希望处理入站消息的方法。Java WebSocket API中的消息事件伴随的信息是Session对象,EndpointConfig对象,打开阶段握手中从匹配入站URI过程中获取的路径参数以及最重要的消息本身。连接上的消息将以3种基本形式抵达:文本消息二进制消息Pong消息
// 文本消息处理方法
@OnMessage
public void handleTextMessage(String textmessage{
    // process the textMessage here
}

// 文本消息高级选项:分批接收文本
@OnMessage
public void catchDocumentPart(String text, boolean isLast){
    // process the textMessage here
}

// 二进制消息处理方法
@OnMessage
public void processBinary(byte[] messageData, Session session) {
    // process binary data here
}

// 二进制消息高级选项:分批接收二进制数据
@OnMessage
public void processVideoFragment(byte[] partialData, boolean isLast){
    if (!isLast){
        // there is more to come;
    } else {
        // now we have the whole message;
    }
}

// 使用java.io.InputStream来处理二进制消息
@OnMessage
public void handleBinary(InputStream is){
    // read
}

// 同理使用java.io.Reader处理文本消息

// Pong消息
@OnMessage
public void processPong(PongMessage message){
    // process pong 
}

事实上,处理消息还有更多的选项:你甚至可以让WebSocket实现把入站消息转换成自己选择的对象。

WebSocket应用一般是异步的双向消息。换言之,典型应用并不总是立即响应入站消息。尽管如此,在一些场景下你希望立刻响应入站消息。因此,通过@OnMessage注解的此类方法上有一个额外选项:方法可以有返回类型或者返回为空。当使用@OnMessage注解的方法有返回类型时,WebSocket实现立即将返回值作为消息返回给刚刚在方法中处理的消息的发送者。

  • @OnError 此注解可以用来注解WebSocket端点的方法,使其可以处理WebSocket实现处理入站消息时发生的任何错误。
@Error
public void errorHandler(Throwable t){
    // log error here
}
  • @OnClose 可以用来注解多种不同类型的方法来处理关闭事件。伴随关闭事件的信息是关闭信息,与建立连接的打开阶段握手相关联的任意一个路径参数,以及一些描述连接关闭原因的信息。
@OnClose
public void goodbye(CloseReason cr){
    // log the reason for posterity
    // close database connection
}

以下是完整示例

import java.io.*;
import java.io.IOException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/lights")
public class LifecycleEndpoint {
    private static String START_TIME = "Start Time";
    private Session session;

    @OnOpen
    public void whenOpening(Session session){
        this.session = session;
    session.getUserProperties().put(START_TIME, System.currentTimeMillis());
    this.sendMessage("3:Just opened");
    }

    @OnMessage
    public void whenGettingAMessage(String message){
        if (message.indexOf("xxx") != -1){
        throw new IllegalArgumentException("xxx not allowed !!");
    } else if (message.indexOf("close") != -1){
        try {
            this.sendMessage("1:Server closing after "+this.getConnectionSeconds()+"s");
        session.close();
        } catch (IOException ioe){
            System.out.println("Error closing session "+ioe.getMessage());
        }
        return; 
    }
    this.sendMessage("3:Just processed a message");
    }

    @OnError
    public void whenSomethingGoesWrong(Throwable t){
        this.sendMessage("2:Error:"+t.getMessage());
    }

    @OnClose
    public void whenClosing(){
        System.out.println("Goodbye !");
    }

    void sendMessage(String message){
        try {
        session.getBasicRemote().sendText(message);
    } catch (Throwable ioe){
        System.out.println("Error sending message "+ioe.getMessage());
    }
    }

    int getConnectionSeconds(){
        long millis = System.currentTimeMillis()-((Long)this.session.getUserProperties().get(START_TIME));
    return (int)millis/1000;
    }
}

编程式端点生命周期

生命周期事件
事件端点方法
打开public abstract void onOpen(Session session, EndpointConfig config)
错误public void onError(Session session, Throwable thr)
关闭public void onClose(Session session, CloseReason cr)
处理消息

为了处理入站消息,需要提供MessageHandler实现。
+ 对于文本消息,使用MessageHandler.Whole
+ 对于二进制消息,使用MessageHandler.Whole

一旦你实现了上述一个或者两个接口来定义希望消费者消息的方式,你需要做的所有事情是通过调用

session.addMessageHandler(myMessageHandler handler)

在第一个消息到达之前的某一时刻注册你的消息处理程序到代表你有兴趣侦听的连接的Session对象上。通常,端点将在onOpen()方法中添加其消息处理程序,因此可以确保不遗漏任何消息

以下是编程式的实现

import java.io.IOException;
import javax.websocket.CloseReason;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;

public class ProgrammaticLifecycleEndpoint extends Endpoint {
    private static String START_TIME = "Start Time";
    private Session session;

    @Override
    public void onOpen(Session session, EndpointConfig config){
        this.session = session;
    final Session mySession = session;
    this.session.addMessageHandler(new MessageHandler.Whole<String>(){
        @Override
        public void onMessage(String message){
            if (message.indexOf("xxx") != -1){
            throw new IllegalArgumentException("xxx not allowed !");
        } else if (message.indexOf("close") != -1){
            try {
                sendMessage("1:Server closing after "+getConnectionSecondes()+"s");
            mySession.close();
            } catch (IOException e){
                System.out.println("Error closing session "+e.getMessage());
            }
            return;
        }
        sendMessage("3:Just processed a message");
        }
    });
    session.getUserProperties().put(START_TIME, System.currentTimeMillis());
    this.sendMessage("3:Just opened");
    }

    @Override
    public void onClose(Session session, CloseReason reason){
        System.out.println("Goodbye !");
    }

    @Override
    public void onError(Session session, Throwable thr){
        this.sendMessage("2:Error:"+thr.getMessage());
    }

    void sendMessage(String message){
        try {
        session.getBasicRemote().sendText(message);
    } catch (IOException e){
        System.out.println("Error sending message "+message);
    }
    }

    int getConnectionSeconds(){
        long millis = System.currentTimeMillis()-((Long)this.session.getUserProperties().get(START_TIME));
    return (int)millis/1000;
    }

}

实例数目及线程机制

上述实现Lifecycle时,将会话存储为实例变量的原因是,我们可以使用它来阐述WebSocket端点生命周期中的一个更重要的问题。如果你重新启动Lifecycle应用,但是这一次打开第二个浏览器窗口到同样的首页,你将看到两组交通信号灯。假如你开始按下任意一个浏览器窗口的生命周期按钮,那么将看到每组信号灯都可以是不同的状态。这是因为每个浏览器窗口对Lifecycle WebSocket端点来说都充当一个独立的客户端,并且WebSocket实现为每个连接的客户端使用不同的LifecycleEndpoint实例。
这意味着对于每一个WebSocket端点(不管是注解式还是编程式)定义来说,WebSocket容器在每次有新的客户端连接时会实例化端点的一个新的实例。这样做的结果是每个WebSocket端点实例仅能够永远看到同样的会话实例:此实例表示从唯一的客户端连接到那个端点实例的唯一连接。

WebSocket实现也为你提供了另外一个重要的保证:同一个会话(或者是连接)中不允许两个事件线程同时调用一个端点实例。这可能听起来很抽象,但是这意味着端点实例永远不会在某时被WebSocket实现的一个以上的线程调用。它意味着如果客户端发送多条消息,WebSocket实现必须调用端点每次处理一条消息。知道这一点特别重要,因为这意味着你永远不需要担心为端点实例的并发访问进行编程。这也是Java WebSocket编程模型与Java Servlet编程模型的关键差异,Java Servlet实例可能被多个线程同时调用,每个线程用于处理不同客户端的请求/响应交互。这意味着WebSocket编程明显更加容易。

消息通信基础

消息通信概述

发送消息

RemoteEndpoint接口和它的子类(RemoteEndpoint.Basic和RemoteEndpoint.Async)提供了发送消息的所有方法。

public void sendPing(ByteBuffer applicationData){
    throws IOException, IllegalArgumentException
}
发送字符串消息

RemoteEndpoint.Basic API提供了3中发送字符串的方法
+ 最简单的方法

// RemoteEndpoint.Basic 发送文本消息
public void sendText(String text) throws IOException
  • 由于WebSocket消息通常表现为一些高层级的对象形式(序列化成String以便发送),因此Java WebSocket API也提供了一种使用Write API发送String消息的方式
// RemoteEndpoint.Basic 发送文本消息到流
public Write getSendStream() throws IOException
  • WebSocket协议允许把大的WebSocket消息分解成多个小片段
// RemoteEndpoint以片段形式发送文本消息
public void sendText(String partialMessage, boolean isLast) throws IOException
发送二进制消息

有RemoteEndpoint.Basic接口同步发送消息,也有RemoteEndpoint.Async接口异步发送消息,这里介绍第一种,同样是3种方式

public void sendBinary(ByteBuffer data) throws IOException
// 分片段发送
public void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException

最后,可以得到一个用来写入二进制消息数据的java.io.OutputStream的引用。这非常有用,特别是当使用直接将数据对象写入Java I/O的API时。在完成消息写入后需要关闭输入流。一旦关闭输入流,消息就会被发送。

public OutputStream getSendStream() throws IOException
发送Java对象消息

可以使用RemoteEndpoint.Basic发送任意Java对象消息

public void sendObject(Object data) throws IOException, EncodeException
  • 传一个Java基本类型(或者其等值装箱类),则WebSocket实现会把数据转换成一个标准的Java字符串(就是使用toString()方法)
  • 传入的是其它对象,那么需要为WebSocket实现提供一个javax.websocket.Encoder接口的实现。在Encoder家族中,最通用的接口是javax.websocket.Encoder.Text,T就是你想发送的对象的类型。
public String encode(T object) throws EncodeException
  • 如果想把对象编码成WebSocket二进制消息,可以实现Encoder.Binary接口。
  • 如果想把对象编码成Java I/O流,可以实现Encoder.CharacterStream或者Encoder.BinaryStream

每次使用RemoteEndpoint.Basic的sendObject方法发送T类型的对象时,WebSocket实现都会调用相应的编码器。发送給远程端点的实际上是encode()方法返回的字符串。如果你的编码器无法把指定对象转换成字符串,很可能会抛出EncodeException异常。在这种情形下,EncodeException将会传播给RemoteEndpoint.Basic的sendObject()方法

在端点上配置编码器
  • 对于注解式端点,所有需要做的就是了类级别的WebSocket注解上声明Encoder类。一旦这样做,每当你从这个注解式端点上获取一个RemoteEndpoint引用时,都可以直接传一个Apple对象给sendObject()方法,WebSocket实现会使用MyAppleEncoder吧Apple对象编码成WebSocket消息。
@ServerEndpoint(value = "/fruit_trees", encoders = {MyAppleEncoder.class})
  • 对于编程式端点,需要创建EndpointConfig对象,在创建该对象时可以配置编码器类。
List<Class<? extends Encoder>> encoders = new ArrayList<>();
encoders.add(MyAppleEncoder.class);
ClientEndpointConfig config = ClientEndpointConfig.Builder.create().encoders(encoders).build();

编码器接口类型

编码器接口转换主要方法
Encoder.TextT转换成StringString encode(T Object) throws EncodeException
Encoder.TextStreamT转换成Writervoid encode(T object, Writer writer) throws EncodeException, IOException
Encoder.BinaryT转换成ByteBufferByteBuffer encode(T object) throws EncodeException
Encoder.BinaryStreamT转换成OutputStreamvoid encode(T object, OutputStream os) throws EncodeException, IOException

接收WebSocket消息

在注解式端点中接收WebSocket消息
// 用@OnMessage处理文本消息
@OnMessage
public void handleTextMessages(String textMessage){
    return "I got this "+textMessage + "!";
}

// 用@OnMessage处理到达的文本消息片段
@OnMessage
public void handlePartial(Sting textMessage, boolan isLast)

// 用@OnMessage处理二进制消息
@OnMessage
public String handleBinaryMessages(byte[] messageData){
    return "I got "+messageData.length+" bytes of data !";
}

// 用@OnMessage处理Pong消息
public String handlePongMessages(PongMessage pongMessage){
    return "I got a pong message carrying "+pongMessage.getApplicationData().length+" bytes of data !";
}

// 用@OnMessage处理Java对象消息,同时必须提供一个Decoder接口的实现。
@OnMessage
public void addToBasket(Orange orange){
    this.bag.addShoppingItem(orange);
    this.cost = this.cost + orange.getPrice();
}

// Decoder.Text<Orange>接口的decode()方法签名
public Orange decode(String rawMessage) throws DecodeEexception

/* 
 * Decoder.Text<Orange>接口的willDeocde()方法签名,在WebSocket实现中,该方法先于decode()方法被调用,
 * 这是为了使你有一个跳过解码消息的机会,例如当消息格式明显不正确时
 *
 */
public boolean willDecode(String s)

为什么不侦听入站Ping消息?答案是Java WebSocket API没有提供这样的方法。WebSocket实现被要求以最快的速度回复连接中入站的任何Ping消息,Pong消息包含的数据与Ping消息相同,因此不另外写代码侦听Ping消息。

接收方法参数类型

参数类型处理的消息类型示例
String文本消息public void handle(String message)
String, boolean文本消息片段public void handle(String parialMessage, boolean isLast)
Reader文本消息流public void handle(Reader message)
byte[]二进制消息public void handle(byte[] data)
ByteBuffer二进制消息public void handle(ByteBuffer data)
byte[], boolean二进制消除片段public void handle(byte[] partialData, boolean isLast)
ByteBuffer, boolean二进制消息片段public void handle(ByteBuffer partialData, boolean isLast)
PongMessagePong消息public void handle(PongMessage message)

解码器接口

解码器接口转换主要解码方法
Decoder.TextString转换成TT decode(String raw) throws DecodeException
Decoder.TextStreamReader转换成TT decode(Reader raw) throws DecodeException
Decoder.BinarByteBuffer转化成TT decode(ByteBuffer raw) throws DecodeException
Decoder.BinaryStreamInputStream转换成TT decode(InputStream raw) throws DecodeException

在你的WebSocket端点中,应始终包含错误处理方法。除了处理入站消息的错误之外,端点上的其他WebSocket方法(如打开事件处理方法)产生的运行时异常也会被传递到这里。如果没有错误处理方法,你可能不知道消息是否已经到达过

Java WebSocket API提供了一个方便,为Java基本类型和它的等价类提供了内置文本解码器。WebSocket实现采取的途径就是使用基本类型或者等价类转换成其等价类,使用单个字符串参数的构造函数生成等价类。

@OnMessage
public void doCount(Interger message){
    // process Integer
}

Java对象消息的传递选项

传递选项解码器示例
Java基本类型及其等价类的文本消息自动@OnMessage public void handleTransferCode(Double d)
自定义Java对象的文本或者二进制消息开发人员提供@OnMessage public void handleObject(CustomObject o)

注解了@OnMessage的方法提供了返回值,返回值的类型决定了WebSocket实现要寄回给消息发送者的WebSocket消息的类型。为了能回应一个文本消息,返回类型为String;为了回应一个二进制消息,返回类型应为byte[]或者ByteBuffer。这意味着在注解式端点中,以下代码都是有效的消息处理方法

@OnMessage
public String echo(String message){...}

@OnMessage
public Integer processAndConfirm(byte[] uplaod){...}

@OnMessage
public boolean purchase(String item){...}

在注解式端点上,Java WebSocket实现为了能够将入站消息分配到正确的消息处理方法上,它设置了一个非常严格的限制:每个注解式端点最多只有一个消息处理方法处理每种本地WebSocket消息类型(即文本消息,二进制消息和Pong消息)

严正声明:从这里开始,不再更新编程式,只更新注解式

综合应用

DrawingObject类

public class DrawingObject {

    public static String MESSAGE_NAME = "DrawingObject";
    private Shape shape;
    private Point center;
    private int radius = 0;
    private Color color;

    public DrawingObject(Shape shape, Point center, int radius, Color color){}

    // getter ...

    public void draw(Graphics g){}
}

DrawingClient类

@ClientEndpoint (
    decoders = {DrawingDecoder.class},
    encoders = {DrawingEncoder.class}
)
public class DrawingClient {
    private Session session;
    private DrawingWindow window;

    /*
     * 创建客户端端点时必须传入一个DrawingWindow对象,客户端端点被构造好后,把DrawingWindow引用
     * 保存为一个私有实例变量供后续使用。同时通过@ClientEndpoint注解中配置解码器
     */
    public DrawingClient(DrawingWindow window){}

    /*
     * 当建立与服务器的WebSocket连接时,这个方法将传入的Session对象保存为私有实例变量供后续使用
     */
    @OnOpen
    public void init(Session session){}

    /*
     * 因为这个WebSocket端点为DrawingObject配置了解码器,所以能够把DrawingObject对象作为drawingChanged
     * 方法的一个参数。因此,在这种情况下,当收到WebSocket消息时,会把WebSocket消息转换成DrawingObject对象,
     * 这个方法会被调用。
     */
    @OnMessage
    public void drawingChanged(DrawingObject drawingObject){}

    /*
     * 通过调用RemoteEndpoint的sendObject()方法,给DrawingBoard应用的服务端发送了一个DrawingObject实例。
     * 在这背后,WebSocket实现会使用在类级别@ClientEndpoint注解中提供的解码器,也就是DrawingEncoder实例。
     * 为了把DrawingObject实例转换成WebSocket消息,在运行时会调用DrawingEncoder的encode方法
     */
    public void notifyServerDrawingChanged(DrawingObject drawingObject){
        try {
        this.session.getBasicRemote().sendObject(drawingObject);
    } catch (IOException ioe){
        System.out.println("Error: IO "+ioe.getMessage());
    } catch (EncodeException ee){
        System.out.println("Error encoding object :"+ee.getObject());
    }
    }

    /*
     * 在DrawingClient中显式地处理在解码入站消息时产生的这种错误。当然,在真正的应用中,
     * 这样的错误处理只是简单地打印输出错误信息,但handleError()方法会告诉你在代码中如何区分这些错误
     */
    @OnError
    public void handleError(Throwable thw){
        if (thw instanceof DecodeException){
        System.out.println("Error decoding incoming message : "+((DecodeException)thw).getText());
    } else {
        System.out.println("Client WebSocket error : "+thw.getMessage());
    }
    }

    /*
     * 该方法实现的核心是WebSocketContainer类的connectToServer()方法,WebSocketContainer类用于客户端
     * 端点的实例发布到提供的URL上
     */
    public static DrawingClient connect(DrawingWindow window, String path){}

    public void disconnect();
}

接下来是编码类实现

public class DrawingEncoder implements Encoder.Text<DrawingObject>{
    @Override
    public void init(EndpointConfig config){}

    @Override
    public void destroy(){}

    @Override
    public String encode(DrawingObject drawingObject) throws EncodeException {
        // 
    }

}

解码类实现

public class DrawingDecoder implements Decoder.Text<DrawingObject>{

    @Override
    public void init(EndpointConfig config){}

    @Override
    public void destroy(){}

    @Override
    public DrawingObject decode(String s) throws DecodeException {
        //
    }

    /*
     * 该方法负责对文本消息作初步的检查,判断这些消息自己是否能解码。如果willDecode()方法
     * 没有返回true,WebSocket实现就不会调用decode()方法,消息也不会以DrawingObject形式被传递。
     */
    @Override
    public boolean willDecode(String s){
        return s.startsWith(DrawingObject.MESSAGE_NAME);
    }

}

我们看到解码器和编码器接口定义了一些它自己的生命周期。当每个编码器实例在准备服务和完成服务时,都会调用init(EndpointConfig config)和destroy()方法。如果你实现的编码器需要初始化或者清理很昂贵的资源时,这些方法就很有用。就像端点自身一样,WebSocket实例会为每个对等连接创建一个编码器实例。因此,在这个DrawingEncoder中,生命周期方法的实现是空的,因为不需要任何资源。

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
WebSocket是一种基于TCP协议实现的全双工通信协议,它可以在客户端和服务器之间建立一个实时的双向通信通道。Python中有许多库可以用来创建WebSocket服务器和客户端,常用的有`websocket`和`autobahn`等。 下面是一个简单的WebSocket服务端的示例代码: ```python import asyncio import websockets async def echo(websocket, path): async for message in websocket: await websocket.send(message) start_server = websockets.serve(echo, "localhost", 8765) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever() ``` 这个例子中,我们使用了`websockets`库来创建一个WebSocket服务器,并定义了一个`echo`函数来处理客户端发送的消息。在这个函数中,我们使用`async for`循环来获取客户端发送的消息,并调用`await websocket.send(message)`方法将消息原样返回给客户端。 要启动这个服务端,只需要执行上面的代码即可。 如果要创建一个WebSocket客户端,可以使用`websockets`库中的`connect`方法来连接WebSocket服务器,示例代码如下: ```python import asyncio import websockets async def hello(): async with websockets.connect( 'ws://localhost:8765') as websocket: name = input("What's your name? ") await websocket.send(name) print(f"> {name}") greeting = await websocket.recv() print(f"< {greeting}") asyncio.get_event_loop().run_until_complete(hello()) ``` 在这个例子中,我们使用`async with`语句来创建一个WebSocket连接,并使用`await websocket.send(message)`方法发送消息给服务器。然后使用`await websocket.recv()`方法接收服务器返回的消息。 以上是Python中使用WebSocket的一些基础知识,希望对你有所帮助。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

newcih

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值