首页 文章

在服务器端使用Spring框架在android中设置Stomp客户端

提问于
浏览
8

我正在开发一个Android应用程序,它与Spring中配置的jetty服务器交换数据 . 为了获得更动态的Android应用程序,我试图使用带有Stomp消息的WebSocket协议 .

为了实现这个功能,我在Spring中配置了一个Web套接字消息代理:

@Configuration
//@EnableScheduling
@ComponentScan(
        basePackages="project.web",
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, value = Configuration.class)
)
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/message");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/client");
    }
}

和一个 SimpMessageSendingOperations 在Spring控制器中从服务器向客户端发送消息:

@Controller
public class MessageAddController {
    private final Log log = LogFactory.getLog(MessageAddController.class);

    private SimpMessageSendingOperations messagingTemplate;

    private UserManager userManager;

    private MessageManager messageManager;

    @Autowired
    public MessageAddController(SimpMessageSendingOperations messagingTemplate, 
            UserManager userManager, MessageManager messageManager){
        this.messagingTemplate = messagingTemplate;
        this.userManager = userManager;
        this.messageManager = messageManager;
    }

    @RequestMapping("/Message/Add")
    @ResponseBody
    public SimpleMessage addFriendship(
            @RequestParam String content,
            @RequestParam Long otherUser_id
    ){
        if(log.isInfoEnabled())
            log.info("Execute MessageAdd action");
        SimpleMessage simpleMessage;

        try{
            User curentUser = userManager.getCurrentUser();
            User otherUser = userManager.findUser(otherUser_id);

            Message message = new Message();
            message.setContent(content);
            message.setUserSender(curentUser);
            message.setUserReceiver(otherUser);

            messageManager.createMessage(message);          
            Message newMessage = messageManager.findLastMessageCreated();

            messagingTemplate.convertAndSend( 
                    "/message/add", newMessage);//send message through websocket

            simpleMessage = new SimpleMessage(null, newMessage);
        } catch (Exception e) {
            if(log.isErrorEnabled())
                log.error("A problem of type : " + e.getClass() 
                        + " has occured, with message : " + e.getMessage());
            simpleMessage = new SimpleMessage(
                            new SimpleException(e.getClass(), e.getMessage()), null);
        }
        return simpleMessage;
    }
}

当我在带有stomp.js的Web浏览器中测试此配置时,我没有任何问题:在Web浏览器和Jetty服务器之间完美地交换消息 . 用于Web浏览器测试的JavaScript代码:

var stompClient = null;

    function setConnected(connected) {
        document.getElementById('connect').disabled = connected;
        document.getElementById('disconnect').disabled = !connected;
        document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
        document.getElementById('response').innerHTML = '';
    }

    function connect() {
        stompClient = Stomp.client("ws://YOUR_IP/client");         
        stompClient.connect({}, function(frame) {
            setConnected(true);
            stompClient.subscribe('/message/add', function(message){
                showMessage(JSON.parse(message.body).content);
            });
        });
    }

    function disconnect() {
        stompClient.disconnect();
        setConnected(false);
        console.log("Disconnected");
    }


    function showMessage(message) {
        var response = document.getElementById('response');
        var p = document.createElement('p');
        p.style.wordWrap = 'break-word';
        p.appendChild(document.createTextNode(message));
        response.appendChild(p);
    }

当我尝试在Android中使用gozirra,activemq-stomp等库时,会出现问题:大多数情况下,与服务器的连接不起作用 . 我的应用程序停止运行,几分钟后,我在logcat中有以下消息: java.net.UnknownHostException: Unable to resolve host "ws://192.168.1.39/client": No address associated with hostname ,我不明白为什么 . 代码使用Gozzira库管理我的android活动中的stomp吸引力:

private void stomp_test() {
    String ip = "ws://192.172.6.39/client";
    int port = 8080;

    String channel = "/message/add";
    Client c;

    try {
        c = new Client( ip, port, "", "" );
        Log.i("Stomp", "Connection established");
        c.subscribe( channel, new Listener() {
            public void message( Map header, String message ) {
                Log.i("Stomp", "Message received!!!");
              }
        });

    } catch (IOException ex) {
        Log.e("Stomp", ex.getMessage());
        ex.printStackTrace();

    } catch (LoginException ex) {
        Log.e("Stomp", ex.getMessage());
        ex.printStackTrace();
    } catch (Exception ex) {
        Log.e("Stomp", ex.getMessage());
        ex.printStackTrace();
    }

}

经过一些研究,我发现大多数想要使用Java客户端使用websocket的人使用ActiveMQ服务器,就像在_1711081中一样 . 但是 spring 工具使用起来非常简单,如果我能像现在这样保持我的服务器层,那将会很酷 . 有人会知道如何在服务器端使用Spring配置在客户端使用stomp java(Android)?

3 回答

  • 1

    我使用RxJava https://github.com/NaikSoftware/StompProtocolAndroid为Android(或普通java)实现STOMP协议 . 使用SpringBoot在STOMP服务器上测试 . 简单示例(使用retrolambda):

    private StompClient mStompClient;
    
     // ...
    
     mStompClient = Stomp.over(WebSocket.class, "ws://localhost:8080/app/hello/websocket");
     mStompClient.connect();
    
     mStompClient.topic("/topic/greetings").subscribe(topicMessage -> {
         Log.d(TAG, topicMessage.getPayload());
     });
    
     mStompClient.send("/app/hello", "My first STOMP message!");
    
     // ...
    
     mStompClient.disconnect();
    

    在项目中添加以下类路径:

    classpath 'me.tatarka:gradle-retrolambda:3.2.0'
    

    在app build.gradle中添加以下内容:

    apply plugin: 'me.tatarka.retrolambda'
    
    android {
           .............
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }
    
    
      dependencies {
          ............................
        compile 'org.java-websocket:Java-WebSocket:1.3.0'
        compile 'com.github.NaikSoftware:StompProtocolAndroid:1.1.5'
    }
    

    全部异步工作!您可以在 subscribe()send() 之后调用 connect() ,消息将被推送到队列 .

    附加功能:

    • 用于握手查询的额外HTTP标头(用于传递身份验证令牌或其他)

    • 您可以为库实现自己的传输,只需实现接口ConnectionProvider

    • 订阅连接生命周期事件(已连接,已关闭,错误)

    例如 :

    public class MainActivity extends AppCompatActivity {
        private StompClient mStompClient;
        public static  final String TAG="StompClient";
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Button view = (Button) findViewById(R.id.button);
            view.setOnClickListener(e->  new LongOperation().execute(""));
    
    
        }
    
    
            private class LongOperation extends AsyncTask<String, Void, String> {
            private StompClient mStompClient;
            String TAG="LongOperation";
            @Override
            protected String doInBackground(String... params) {
    
                mStompClient = Stomp.over(WebSocket.class, "ws://localhost:8080/app/hello/websocket");
                mStompClient.connect();
    
                mStompClient.topic("/topic/greetings").subscribe(topicMessage -> {
                    Log.d(TAG, topicMessage.getPayload());
                });
    
                mStompClient.send("/app/hello", "My first STOMP message!").subscribe();
    
    
                mStompClient.lifecycle().subscribe(lifecycleEvent -> {
                    switch (lifecycleEvent.getType()) {
    
                        case OPENED:
                            Log.d(TAG, "Stomp connection opened");
                            break;
    
                        case ERROR:
                            Log.e(TAG, "Error", lifecycleEvent.getException());
                            break;
    
                        case CLOSED:
                            Log.d(TAG, "Stomp connection closed");
                            break;
                    }
                });
                return "Executed";
            }
    
            @Override
            protected void onPostExecute(String result) {
    
            }
    
        }
    }
    

    在manifest.xml中添加Internet权限

    <uses-permission android:name="android.permission.INTERNET" />
    
  • 6

    我实现了使用Android和Spring服务器在web套接字上使用stomp .

    要做这样的事情,我使用了一个Web套接字库:werbench(按照这个link下载它) . 要安装,我使用了maven命令 mvn install ,然后我在本地存储库中找回了jar . 然后,我需要在基本的网络套接字上添加一个stomp图层,但是我不能犹豫地问我是否要看看它,但我很快意识到这一点,所以它不能像stomp一样管理 . JS . 然后,我需要用我的spring服务器实现身份验证 . 为实现这一目标,我遵循了this site的指示 . 当我回到JSESSIONID cookie时,我只需要在我的stomp "library"中的werbench web套接字的实例化中声明带有此cookie的头 .

    编辑:这是这个库中的主类,管理web套接字连接的主要类:

    import java.io.IOException;
    import java.net.URI;
    import java.net.URISyntaxException;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    import android.util.Log;
    import de.roderick.weberknecht.WebSocket;
    import de.roderick.weberknecht.WebSocketEventHandler;
    import de.roderick.weberknecht.WebSocketMessage;
    
    public class Stomp {
    
        private static final String TAG = Stomp.class.getSimpleName();
    
        public static final int CONNECTED = 1;//Connection completely established
        public static final int NOT_AGAIN_CONNECTED = 2;//Connection process is ongoing
        public static final int DECONNECTED_FROM_OTHER = 3;//Error, no more internet connection, etc.
        public static final int DECONNECTED_FROM_APP = 4;//application explicitely ask for shut down the connection 
    
        private static final String PREFIX_ID_SUBSCIPTION = "sub-";
        private static final String ACCEPT_VERSION_NAME = "accept-version";
        private static final String ACCEPT_VERSION = "1.1,1.0";
        private static final String COMMAND_CONNECT = "CONNECT";
        private static final String COMMAND_CONNECTED = "CONNECTED";
        private static final String COMMAND_MESSAGE = "MESSAGE";
        private static final String COMMAND_RECEIPT = "RECEIPT";
        private static final String COMMAND_ERROR = "ERROR";
        private static final String COMMAND_DISCONNECT = "DISCONNECT";
        private static final String COMMAND_SEND = "SEND";
        private static final String COMMAND_SUBSCRIBE = "SUBSCRIBE";
        private static final String COMMAND_UNSUBSCRIBE = "UNSUBSCRIBE";
        private static final String SUBSCRIPTION_ID = "id";
        private static final String SUBSCRIPTION_DESTINATION = "destination";
        private static final String SUBSCRIPTION_SUBSCRIPTION = "subscription";
    
    
        private static final Set<String> VERSIONS = new HashSet<String>();
        static {
            VERSIONS.add("V1.0");
            VERSIONS.add("V1.1");
            VERSIONS.add("V1.2");
        }
    
        private WebSocket websocket;
    
        private int counter;
    
        private int connection;
    
        private Map<String, String> headers;
    
        private int maxWebSocketFrameSize;
    
        private Map<String, Subscription> subscriptions;
    
        private ListenerWSNetwork networkListener;
    
        /**
         * Constructor of a stomp object. Only url used to set up a connection with a server can be instantiate
         * 
         * @param url
         *      the url of the server to connect with
         */
        public Stomp(String url, Map<String,String> headersSetup, ListenerWSNetwork stompStates){       
            try {
                this.websocket = new WebSocket(new URI(url), null, headersSetup);
                this.counter = 0;
    
                this.headers = new HashMap<String, String>();
                this.maxWebSocketFrameSize = 16 * 1024;
                this.connection = NOT_AGAIN_CONNECTED;
                this.networkListener = stompStates;
                this.networkListener.onState(NOT_AGAIN_CONNECTED);
                this.subscriptions = new HashMap<String, Subscription>();
    
                this.websocket.setEventHandler(new WebSocketEventHandler() {        
                    @Override
                    public void onOpen(){
                        if(Stomp.this.headers != null){                                         
                            Stomp.this.headers.put(ACCEPT_VERSION_NAME, ACCEPT_VERSION);
    
                            transmit(COMMAND_CONNECT, Stomp.this.headers, null);
    
                            Log.d(TAG, "...Web Socket Openned");
                        }
                    }
    
                    @Override
                    public void onMessage(WebSocketMessage message) {
                        Log.d(TAG, "<<< " + message.getText());
                        Frame frame = Frame.fromString(message.getText());
                        boolean isMessageConnected = false;
    
                        if(frame.getCommand().equals(COMMAND_CONNECTED)){
                            Stomp.this.connection = CONNECTED;
                            Stomp.this.networkListener.onState(CONNECTED);
    
                            Log.d(TAG, "connected to server : " + frame.getHeaders().get("server"));
                            isMessageConnected = true;
    
                        } else if(frame.getCommand().equals(COMMAND_MESSAGE)){
                            String subscription = frame.getHeaders().get(SUBSCRIPTION_SUBSCRIPTION);
                            ListenerSubscription onReceive = Stomp.this.subscriptions.get(subscription).getCallback();
    
                            if(onReceive != null){
                                onReceive.onMessage(frame.getHeaders(), frame.getBody());
                            } else{
                                Log.e(TAG, "Error : Subscription with id = " + subscription + " had not been subscribed");
                                //ACTION TO DETERMINE TO MANAGE SUBCRIPTION ERROR
                            }
    
                        } else if(frame.getCommand().equals(COMMAND_RECEIPT)){
                            //I DON'T KNOW WHAT A RECEIPT STOMP MESSAGE IS
    
                        } else if(frame.getCommand().equals(COMMAND_ERROR)){
                            Log.e(TAG, "Error : Headers = " + frame.getHeaders() + ", Body = " + frame.getBody());
                            //ACTION TO DETERMINE TO MANAGE ERROR MESSAGE
    
                        } else {
    
                        }
    
                        if(isMessageConnected)
                            Stomp.this.subscribe();
                    }
    
                    @Override
                    public void onClose(){
                        if(connection == DECONNECTED_FROM_APP){
                            Log.d(TAG, "Web Socket disconnected");
                            disconnectFromApp();
                        } else{
                            Log.w(TAG, "Problem : Web Socket disconnected whereas Stomp disconnect method has never "
                                    + "been called.");
                            disconnectFromServer();
                        }
                    }
    
                    @Override
                    public void onPing() {
    
                    }
    
                    @Override
                    public void onPong() {
    
                    }
    
                    @Override
                    public void onError(IOException e) {
                        Log.e(TAG, "Error : " + e.getMessage());                
                    }
                });
            } catch (URISyntaxException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * Send a message to server thanks to websocket
         * 
         * @param command
         *      one of a frame property, see {@link Frame} for more details
         * @param headers
         *      one of a frame property, see {@link Frame} for more details
         * @param body
         *      one of a frame property, see {@link Frame} for more details
         */
        private void transmit(String command, Map<String, String> headers, String body){
            String out = Frame.marshall(command, headers, body);
            Log.d(TAG, ">>> " + out);
            while (true) {
                if (out.length() > this.maxWebSocketFrameSize) {
                    this.websocket.send(out.substring(0, this.maxWebSocketFrameSize));
                    out = out.substring(this.maxWebSocketFrameSize);
                } else {
                    this.websocket.send(out);
                    break;
                }
            }
        }
    
        /**
         * Set up a web socket connection with a server
         */
        public void connect(){
            if(this.connection != CONNECTED){
                Log.d(TAG, "Opening Web Socket...");
                try{
                    this.websocket.connect();
                } catch (Exception e){
                    Log.w(TAG, "Impossible to establish a connection : " + e.getClass() + ":" + e.getMessage());
                }
            }
        }
    
        /**
         * disconnection come from the server, without any intervention of client side. Operations order is very important
         */
        private void disconnectFromServer(){
            if(this.connection == CONNECTED){
                this.connection = DECONNECTED_FROM_OTHER;
                this.websocket.close();
                this.networkListener.onState(this.connection);
            }
        }
    
        /**
         * disconnection come from the app, because the public method disconnect was called
         */
        private void disconnectFromApp(){
            if(this.connection == DECONNECTED_FROM_APP){
                this.websocket.close();
                this.networkListener.onState(this.connection);
            }
        }
    
        /**
         * Close the web socket connection with the server. Operations order is very important
         */
        public void disconnect(){
            if(this.connection == CONNECTED){
                this.connection = DECONNECTED_FROM_APP;
                transmit(COMMAND_DISCONNECT, null, null);
            }
        }
    
        /**
         * Send a simple message to the server thanks to the body parameter
         * 
         * 
         * @param destination
         *      The destination through a Stomp message will be send to the server
         * @param headers
         *      headers of the message
         * @param body
         *      body of a message
         */
        public void send(String destination, Map<String,String> headers, String body){
            if(this.connection == CONNECTED){
                if(headers == null)
                    headers = new HashMap<String, String>();
    
                if(body == null)
                    body = "";
    
                headers.put(SUBSCRIPTION_DESTINATION, destination);
    
                transmit(COMMAND_SEND, headers, body);
            }
        }
    
        /**
         * Allow a client to send a subscription message to the server independently of the initialization of the web socket.
         * If connection have not been already done, just save the subscription
         * 
         * @param subscription
         *      a subscription object
         */
        public void subscribe(Subscription subscription){
            subscription.setId(PREFIX_ID_SUBSCIPTION + this.counter++);
            this.subscriptions.put(subscription.getId(), subscription);
    
            if(this.connection == CONNECTED){   
                Map<String, String> headers = new HashMap<String, String>();            
                headers.put(SUBSCRIPTION_ID, subscription.getId());
                headers.put(SUBSCRIPTION_DESTINATION, subscription.getDestination());
    
                subscribe(headers);
            }
        }
    
        /**
         * Subscribe to a Stomp channel, through messages will be send and received. A message send from a determine channel
         * can not be receive in an another.
         *
         */
        private void subscribe(){
            if(this.connection == CONNECTED){
                for(Subscription subscription : this.subscriptions.values()){
                    Map<String, String> headers = new HashMap<String, String>();            
                    headers.put(SUBSCRIPTION_ID, subscription.getId());
                    headers.put(SUBSCRIPTION_DESTINATION, subscription.getDestination());
    
                    subscribe(headers);
                }
            }
        }
    
        /**
         * Send the subscribe to the server with an header
         * @param headers
         *      header of a subscribe STOMP message
         */
        private void subscribe(Map<String, String> headers){
            transmit(COMMAND_SUBSCRIBE, headers, null);
        }
    
        /**
         * Destroy a subscription with its id
         * 
         * @param id
         *      the id of the subscription. This id is automatically setting up in the subscribe method
         */
        public void unsubscribe(String id){
            if(this.connection == CONNECTED){
                Map<String, String> headers = new HashMap<String, String>();
                headers.put(SUBSCRIPTION_ID, id);
    
                this.subscriptions.remove(id);
                this.transmit(COMMAND_UNSUBSCRIBE, headers, null);
            }
        }
    }
    

    这是一个Stomp消息的框架:

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    public class Frame {
    //  private final static String CONTENT_LENGTH = "content-length";
    
        private String command;
        private Map<String, String> headers;
        private String body;
    
        /**
         * Constructor of a Frame object. All parameters of a frame can be instantiate
         * 
         * @param command
         * @param headers
         * @param body
         */
        public Frame(String command, Map<String, String> headers, String body){
            this.command = command;     
            this.headers = headers != null ? headers : new HashMap<String, String>();
            this.body = body != null ? body : "";
        }
    
        public String getCommand(){
            return command;
        }
    
        public Map<String, String> getHeaders(){
            return headers;
        }
    
        public String getBody(){
            return body;
        }
    
        /**
         * Transform a frame object into a String. This method is copied on the objective C one, in the MMPReactiveStompClient
         * library
         * @return a frame object convert in a String
         */
        private String toStringg(){
            String strLines = this.command;
            strLines += Byte.LF;
            for(String key : this.headers.keySet()){
                strLines += key + ":" + this.headers.get(key);
                strLines += Byte.LF;
            }
            strLines += Byte.LF;
            strLines += this.body;
            strLines += Byte.NULL;
    
            return strLines;
        }
    
        /**
         * Create a frame from a received message. This method is copied on the objective C one, in the MMPReactiveStompClient
         * library
         * 
         * @param data
         *  a part of the message received from network, which represented a frame
         * @return
         *  An object frame
         */
        public static Frame fromString(String data){
            List<String> contents = new ArrayList<String>(Arrays.asList(data.split(Byte.LF)));
    
            while(contents.size() > 0 && contents.get(0).equals("")){
                contents.remove(0);
            }
    
            String command = contents.get(0);
            Map<String, String> headers = new HashMap<String, String>();
            String body = "";
    
            contents.remove(0);
            boolean hasHeaders = false;
            for(String line : contents){
                if(hasHeaders){
                    for(int i=0; i < line.length(); i++){
                        Character c = line.charAt(i);
                        if(!c.equals('\0'))
                            body += c;
                    }
                } else{
                    if(line.equals("")){
                        hasHeaders = true;
                    } else {
                        String[] header = line.split(":");
                        headers.put(header[0], header[1]);
                    }
                }
            }
            return new Frame(command, headers, body);   
        }
    
    //    No need this method, a single frame will be always be send because body of the message will never be excessive
    //    /**
    //     * Transform a message received from server in a Set of objects, named frame, manageable by java
    //     * 
    //     * @param datas
    //     *        message received from network
    //     * @return
    //     *        a Set of Frame
    //     */
    //    public static Set<Frame> unmarshall(String datas){
    //      String data;
    //      String[] ref = datas.split(Byte.NULL + Byte.LF + "*");//NEED TO VERIFY THIS PARAMETER
    //      Set<Frame> results = new HashSet<Frame>();
    //      
    //      for (int i = 0, len = ref.length; i < len; i++) {
    //            data = ref[i];
    //            
    //            if ((data != null ? data.length() : 0) > 0){
    //              results.add(unmarshallSingle(data));//"unmarshallSingle" is the old name method for "fromString"
    //            }
    //        }         
    //      return results;
    //    }
    
        /**
         * Create a frame with based fame component and convert them into a string
         * 
         * @param command
         * @param headers
         * @param body
         * @return  a frame object convert in a String, thanks to <code>toStringg()</code> method
         */
        public static String marshall(String command, Map<String, String> headers, String body){
            Frame frame = new Frame(command, headers, body);
            return frame.toStringg();
        }
    
        private class Byte {
            public static final String LF = "\n";
            public static final String NULL = "\0";
        }
    }
    

    这个是用于通过stomp协议 Build 订阅的对象:

    public class Subscription {
    
        private String id;
    
        private String destination;
    
        private ListenerSubscription callback;
    
        public Subscription(String destination, ListenerSubscription callback){
            this.destination = destination;
            this.callback = callback;
        }
    
        public String getId() {
            return id;
        }
        public void setId(String id) {
            this.id = id;
        }
    
        public String getDestination() {
            return destination;
        }
    
        public ListenerSubscription getCallback() {
            return callback;
        }
    }
    

    至少,有两个接口用作“Run”java类,用于监听Web套接字网络和给定的订阅管道

    public interface ListenerWSNetwork {
        public void onState(int state);
    }
    
    import java.util.Map;
    public interface ListenerSubscription {
        public void onMessage(Map<String, String> headers, String body);
    }
    

    欲了解更多信息,请不要犹豫,问我 .

  • 7

    完美解决方案Eperrin谢谢 . 我想填写完整的解决方案,例如在您的活动/服务阶段,您调用 connection 方法当然不在MainThread中 .

    private void connection() {
            Map<String,String> headersSetup = new HashMap<String,String>();
            Stomp stomp = new Stomp(hostUrl, headersSetup, new ListenerWSNetwork() {
                @Override
                public void onState(int state) {
                }
            });
            stomp.connect();
            stomp.subscribe(new Subscription(testUrl, new ListenerSubscription() {
                @Override
                public void onMessage(Map<String, String> headers, String body) {
                }
            }));
        }
    

    并且在websocket weberknecht library中小心 WebSocketHandshake class 中的错误方法 verifyServerHandshakeHeaders 中的错误是在行124上检查只有(!headers.get("Connection").equals("Upgrade"))并且当服务器发送升级而不是升级时你得到错误连接失败:服务器握手中缺少 Headers 字段:如果(!headers.get("Connection").equalsIgnoreCase("Upgrade")),则必须关闭忽略大小写的连接

相关问题