日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

WebSocket通信原理和在Tomcat中实现源码详解(万字爆肝)

發布時間:2023/12/18 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 WebSocket通信原理和在Tomcat中实现源码详解(万字爆肝) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

首發CSDN:徐同學呀,原創不易,轉載請注明源鏈接。我是徐同學,用心輸出高質量文章,希望對你有所幫助。 本篇基于Tomcat10.0.6。建議收藏起來慢慢看。

文章目錄

    • 一、前言
    • 二、什么是WebSocket
      • 1、HTTP/1.1的缺陷
      • 2、WebSocket發展歷史
        • (1)背景
        • (2)歷史
      • 3、WebSocket握手和雙向通信
        • (1)定義
        • (2)握手(建立連接)
        • (3)消息幀
        • (4)揮手(關閉連接)
      • 4、WebSocket優點
    • 三、Java API for WebSocket(JSR356)
      • 1、服務端API
        • (1)注解方式@ServerEndpoint
        • (2)繼承抽象類Endpoint
      • 2、客戶端API
      • 3、上下文Session
      • 4、HandshakeRequest 和 HandshakeResponse
        • (1)HandshakeRequest
        • (2)HandshakeResponse
      • 5、WebSocketContainer
    • 四、WebSocket基于Tomcat應用
      • 1、服務器端實現
        • (1)@ServerEndpoint注解方式
        • (2)繼承抽象類Endpoint方式
        • (3)早期Tomcat7中Server端實現對比
      • 2、客戶端實現
        • (1)前端js版
        • (2)@ClientEndpoint注解方式
        • (3)繼承抽象類Endpoint方式
      • 3、基于Nginx反向代理注意事項
    • 五、WebSocket在Tomcat中的源碼實現
      • 1、WsSci初始化
        • (1)WsSci#onStartup
        • (2)WsServerContainer#addEndpoint
        • (3)PojoMethodMapping方法映射和形參解析
      • 2、協議升級(握手)
        • (1)WsFilter
        • (2)UpgradeUtil#doUpgrade
        • (3)Request#upgrade
        • (4)回調機制ActionHook#action
        • (5)ConnectionHandler#process
        • (6)WsHttpUpgradeHandler#init握手成功
      • 3、數據傳輸和解析
        • (1)接收客戶端消息
        • (2)發送消息給客戶端
    • 六、要點回顧
    • 七、參考文獻

一、前言

WebSocket是一種全雙工通信協議,即客戶端可以向服務端發送請求,服務端也可以主動向客戶端推送數據。這樣的特點,使得它在一些實時性要求比較高的場景效果斐然(比如微信朋友圈實時通知、在線協同編輯等)。主流瀏覽器以及一些常見服務端通信框架(Tomcat、netty、undertow、webLogic等)都對WebSocket進行了技術支持。那么,WebSocket具體是什么?為什么會出現WebSocket?如何做到全雙工通信?解決了什么問題?

二、什么是WebSocket

1、HTTP/1.1的缺陷

HTTP/1.1最初是為網絡中超文本資源(HTML),請求-響應傳輸而設計的,后來支持了傳輸更多類型的資源,如圖片、視頻等,但都沒有改變它單向的請求-響應模式。

隨著互聯網的日益壯大,HTTP/1.1功能使用上已體現捉襟見肘的疲態。雖然可以通過某些方式滿足需求(如Ajax、Comet),但是性能上還是局限于HTTP/1.1,那么HTTP/1.1有哪些缺陷呢:

  • 請求-響應模式,只能客戶端發送請求給服務端,服務端才可以發送響應數據給客戶端。
  • 傳輸數據為文本格式,且請求/響應頭部冗長重復。

(為了區分HTTP/1.1和HTTP/1.2,下面描述中,HTTP均代表HTTP/1.1)

2、WebSocket發展歷史

(1)背景

在WebSocket出現之前,主要通過長輪詢和HTTP長連接實現實時數據更新,這種方式有個統稱叫Comet,Tomcat8.5之前有對Comet基于流的HTTP長連接做支持,后來因為WebSocket的成熟和標準化,以及Comet自身依然是基于HTTP,在性能消耗和瓶頸上無法跳脫HTTP,就把Comet廢棄了。

還有一個SPDY技術,也對HTTP進行了改進,多路復用流、服務器推送等,后來演化成HTTP/2.0,因為適用場景和解決的問題不同,暫不對HTTP/2.0做過多解釋,不過對于HTTP/2.0和WebSocket在Tomcat實現中都是作為協議升級來處理的。

(Comet和SPDY的原理不是本篇重點,沒有展開講解,感興趣的同學可自行百度)

(2)歷史

在這種背景下,HTML5制定了WebSocket

  • 籌備階段,WebSocket被劃分為HTML5標準的一部分,2008年6月,Michael Carter進行了一系列討論,最終形成了稱為WebSocket的協議。
  • 2009年12月,Google Chrome 4是第一個提供標準支持的瀏覽器,默認情況下啟用了WebSocket。
  • 2010年2月,WebSocket協議的開發從W3C和WHATWG小組轉移到IETF(TheInternet Engineering Task Force),并在Ian Hickson的指導下進行了兩次修訂。
  • 2011年,IETF將WebSocket協議標準化為RFC 6455起,大多數Web瀏覽器都在實現支持WebSocket協議的客戶端API。此外,已經開發了許多實現WebSocket協議的Java庫。
  • 2013年,發布JSR356標準,Java API for WebSocket。

(為什么要去了解WebSocket的發展歷史和背景呢?個人認為可以更好的理解某個技術實現的演變歷程,比如Tomcat,早期有Comet沒有WebSocket時,Tomcat就對Comet做了支持,后來有WebSocket了,但是還沒出JSR356標準,Tomcat就對Websocket做了支持,自定義API,再后來有了JSR356,Tomcat立馬緊跟潮流,廢棄自定義的API,實現JSR356那一套,這就使得在Tomcat7使用WebSocket的同學,想升為Tomcat8(其實Tomcat7.0.47之后就是JSR356標準了),發現WebSocket接入方式變了,而且一些細節也變了。)

3、WebSocket握手和雙向通信

(1)定義

WebSocket全雙工通信協議,在客戶端和服務端建立連接后,可以持續雙向通信,和HTTP同屬于應用層協議,并且都依賴于傳輸層的TCP/IP協議。

雖然WebSocket有別于HTTP,是一種新協議,但是RFC 6455中規定:

it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries.

  • WebSocket通過HTTP端口80和443進行工作,并支持HTTP代理和中介,從而使其與HTTP協議兼容。
  • 為了實現兼容性,WebSocket握手使用HTTP Upgrade頭從HTTP協議更改為WebSocket協議。
  • Websocket使用ws或wss的統一資源標志符(URI),分別對應明文和加密連接。

(2)握手(建立連接)

在雙向通信之前,必須通過握手建立連接。Websocket通過 HTTP/1.1 協議的101狀態碼進行握手,首先客戶端(如瀏覽器)發出帶有特殊消息頭(Upgrade、Connection)的請求到服務器,服務器判斷是否支持升級,支持則返回響應狀態碼101,表示協議升級成功,對于WebSocket就是握手成功。

客戶端請求示例:

GET /test HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: tFGdnEL/5fXMS9yKwBjllg== Origin: http://example.com Sec-WebSocket-Protocol: v10.stomp, v11.stomp, v12.stomp Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Version: 13
  • Connection必須設置Upgrade,表示客戶端希望連接升級。
  • Upgrade: websocket表明協議升級為websocket。
  • Sec-WebSocket-Key字段內記錄著握手過程中必不可少的鍵值,由客戶端(瀏覽器)生成,可以盡量避免普通HTTP請求被誤認為Websocket協議。
  • Sec-WebSocket-Version 表示支持的Websocket版本。RFC6455要求使用的版本是13。
  • Origin字段是必須的。如果缺少origin字段,WebSocket服務器需要回復HTTP 403 狀態碼(禁止訪問),通過Origin可以做安全校驗。

服務端響應示例:

HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HaA6EjhHRejpHyuO0yBnY4J4n3A= Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15 Sec-WebSocket-Protocol: v12.stomp

Sec-WebSocket-Accept的字段值是由握手請求中的Sec-WebSocket-Key的字段值生成的。成功握手確立WebSocket連接之后,通信時不再使用HTTP的數據幀,而采用WebSocket獨立的數據幀。

(3)消息幀

WebSocket使用二進制消息幀作為雙向通信的媒介。何為消息幀?發送方將每個應用程序消息拆分為一個或多個幀,通過網絡將它們傳輸到目的地,并重新組裝解析出一個完整消息。

有別于HTTP/1.1文本消息格式(冗長的消息頭和分隔符等),WebSocket消息幀規定一定的格式,以二進制傳輸,更加短小精悍。二者相同之處就是都是基于TCP/IP流式協議(沒有規定消息邊界)。

如下是消息幀的基本結構圖:

  • FIN: 1 bit,表示該幀是否為消息的最后一幀。1-是,0-否。
  • RSV1,RSV2,RSV3: 1 bit each,預留(3位),擴展的預留標志。一般情況為0,除非協商的擴展定義為非零值。如果接收到非零值且不為協商擴展定義,接收端必須使連接失敗。
  • Opcode: 4 bits,定義消息幀的操作類型,如果接收到一個未知Opcode,接收端必須使連接失敗。(0x0-延續幀,0x1-文本幀,0x2-二進制幀,0x8-關閉幀,0x9-PING幀,0xA-PONG幀(在接收到PING幀時,終端必須發送一個PONG幀響應,除非它已經接收到關閉幀),0x3-0x7保留給未來的非控制幀,0xB-F保留給未來的控制幀)
  • Mask: 1 bit,表示該幀是否為隱藏的,即被加密保護的。1-是,0-否。Mask=1時,必須傳一個Masking-key,用于解除隱藏(客戶端發送消息給服務器端,Mask必須為1)。
  • Payload length: 7 bits, 7+16 bits, or 7+64 bits,有效載荷數據的長度(擴展數據長度+應用數據長度,擴展數據長度可以為0)。

if 0-125, that is the payload length. If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length. If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the most significant bit MUST be 0) are the payload length.

  • Masking-key: 0 or 4 bytes,用于解除幀隱藏(加密)的key,Mask=1時不為空,Mask=0時不用傳。
  • Payload data: (x+y) bytes,有效載荷數據包括擴展數據(x bytes)和應用數據(y bytes)。有效載荷數據是用戶真正要傳輸的數據。

這樣的二進制消息幀設計,與HTTP協議相比,WebSocket協議可以提供約500:1的流量減少和3:1的延遲減少。

(4)揮手(關閉連接)

揮手相對于握手要簡單很多,客戶端和服務器端任何一方都可以通過發送關閉幀來發起揮手請求。發送關閉幀的一方,之后不再發送任何數據給對方;接收到關閉幀的一方,如果之前沒有發送過關閉幀,則必須發送一個關閉幀作為響應。關閉幀中可以攜帶關閉原因。

在發送和接收一個關閉幀消息之后,就認為WebSocket連接已關閉,且必須關閉底層TCP連接。

除了通過關閉握手來關閉連接外,WebSocket連接也可能在另一方離開或底層TCP連接關閉時突然關閉。

4、WebSocket優點

  • 較少的控制開銷。在連接建立后,服務器和客戶端之間交換數據時,用于協議控制的數據包頭部相對于HTTP請求每次都要攜帶完整的頭部,顯著減少。

  • 更強的實時性。由于協議是全雙工的,所以服務器可以隨時主動給客戶端下發數據。相對于HTTP請求需要等待客戶端發起請求服務端才能響應,延遲明顯更少。

  • 保持連接狀態。與HTTP不同的是,Websocket需要先建立連接,這就使得其成為一種有狀態的協議,之后通信時可以省略部分狀態信息。而HTTP請求可能需要在每個請求都攜帶狀態信息(如身份認證等)。

  • 更好的二進制支持。Websocket定義了二進制幀,相對HTTP,可以更輕松地處理二進制內容。

  • 支持擴展。Websocket定義了擴展,用戶可以擴展協議、實現部分自定義的子協議。

  • 更好的壓縮效果。相對于HTTP壓縮,Websocket在適當的擴展支持下,可以沿用之前內容的上下文,在傳遞類似的數據時,可以顯著提高壓縮率。

三、Java API for WebSocket(JSR356)

JSR356在Java EE7時歸為Java EE標準的一部分(后來Java EE更名為Jakarta EE,世上再無Java EE,以下統一稱Jakarta EE),所有兼容Jakarta EE的應用服務器,都必須遵循JSR356標準的WebSocket協議API。

根據JSR356規定, 建立WebSocket連接的服務器端和客戶端,兩端對稱,可以互相通信,差異性較小,抽象成API,就是一個個Endpoint(端點),只不過服務器端的叫ServerEndpoint,客戶端的叫ClientEndpoint。客戶端向服務端發送WebSocket握手請求,建立連接后就創建一個ServerEndpoint對象。(這里的Endpoint和Tomcat連接器里的AbstractEndpoint名稱上有點像,但是兩個毫不相干的東西,就像周杰倫和周杰的關系。)

ServerEndpoint和ClientEndpoint在API上差異也很小,有相同的生命周期事件(OnOpen、OnClose、OnError、OnMessage),不同之處是ServerEndpoint作為服務器端點,可以指定一個URI路徑供客戶端連接,ClientEndpoint沒有。

1、服務端API

服務器端的Endpoint有兩種實現方式,一種是注解方式@ServerEndpoint,一種是繼承抽象類Endpoint。

(1)注解方式@ServerEndpoint

首先看看@ServerEndpoint有哪些要素:

  • value,可以指定一個URI路徑標識一個Endpoint。
  • subprotocols,用戶在WebSocket協議下自定義擴展一些子協議。
  • decoders,用戶可以自定義一些消息解碼器,比如通信的消息是一個對象,接收到消息可以自動解碼封裝成消息對象。
  • encoders,有解碼器就有編碼器,定義解碼器和編碼器的好處是可以規范使用層消息的傳輸。
  • configurator,ServerEndpoint配置類,主要提供ServerEndpoint對象的創建方式擴展(如果使用Tomcat的WebSocket實現,默認是反射創建ServerEndpoint對象)。

@ServerEndpoint可以注解到任何類上,但是想實現服務端的完整功能,還需要配合幾個生命周期的注解使用,這些生命周期注解只能注解在方法上:

  • @OnOpen 建立連接時觸發。
  • @OnClose 關閉連接時觸發。
  • @OnError 發生異常時觸發。
  • @OnMessage 接收到消息時觸發。

(2)繼承抽象類Endpoint

繼承抽象類Endpoint,重寫幾個生命周期方法。

怎么沒有onMessage方法,實現onMessage還需要繼承實現一個接口jakarta.websocket.MessageHandler,MessageHandler接口又分為Partial和Whole,實現的MessageHandler需要在onOpen觸發時注冊到jakarta.websocket.Session中。

繼承抽象類Endpoint的方式相對于注解方式要麻煩的多,除了繼承Endpoint和實現接口MessageHandler外,還必須實現一個jakarta.websocket.server.ServerApplicationConfig來管理Endpoint,比如給Endpoint分配URI路徑。

而encoders、decoders、configurator等配置信息由jakarta.websocket.server.ServerEndpointConfig管理,默認實現jakarta.websocket.server.DefaultServerEndpointConfig。

所以如果使用 Java 版WebSocket服務器端實現首推注解方式。

2、客戶端API

對于客戶端API,也是有注解方式和繼承抽象類Endpoint方式。

  • 注解方式,只需要將@ServerEndpoint換成@ClientEndpoint。
  • 繼承抽象類Endpoint方式,需要一個jakarta.websocket.ClientEndpointConfig來管理encoders、decoders、configurator等配置信息,默認實現jakarta.websocket.DefaultClientEndpointConfig。

3、上下文Session

WebSocket是一個有狀態的連接,建立連接后的通信都是通過jakarta.websocket.Session保持狀態,一個連接一個Session,每一個Session有一個唯一標識Id。

Session的主要職責涉及:

  • 基礎信息管理(request信息(getRequestURI、getRequestParameterMap、getPathParameters等)、協議版本getProtocolVersion、子協議getNegotiatedSubprotocol等)。
  • 連接管理(狀態判斷isOpen、接收消息的MessageHandler、發送消息的異步遠程端點RemoteEndpoint.Async和同步遠程端點RemoteEndpoint.Basic等)。

4、HandshakeRequest 和 HandshakeResponse

HandshakeRequest 和 HandshakeResponse了解即可,這兩個接口主要用于WebScoket握手升級過程中握手請求響應的封裝,如果只是單純使用WebSocket,不會接觸到這兩個接口。

(1)HandshakeRequest

(2)HandshakeResponse

Sec-WebSocket-Accept根據客戶端傳的Sec-WebSocket-Key生成,如下是Tomcat10.0.6 WebSocket源碼實現中生成Sec-WebSocket-Accept的算法:

private static String getWebSocketAccept(String key) {byte[] digest = ConcurrentMessageDigest.digestSHA1(key.getBytes(StandardCharsets.ISO_8859_1), WS_ACCEPT);return Base64.encodeBase64String(digest); }

5、WebSocketContainer

jakarta.websocket.WebSocketContainer顧名思義,就是WebSocket的容器,集大成者。其主要職責包括但不限于connectToServer,客戶端連接服務器端,基于瀏覽器的WebSocket客戶端連接服務器端,由瀏覽器支持,但是基于Java版的WebSocket客戶端就可以通過WebSocketContainer#connectToServer向服務端發起連接請求。


四、WebSocket基于Tomcat應用

(如下使用的是javax.websocket包,未使用最新的jakarta.websocket,主要是測試項目基于SpringBoot+Tomcat9.x的,Java API for WebSocket版本需要保持一致。)

1、服務器端實現

(1)@ServerEndpoint注解方式

import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong;@ServerEndpoint(value = "/ws/test/{userId}", encoders = {MessageEncoder.class}, decoders = {MessageDecoder.class}, configurator = MyServerConfigurator.class) public class WebSocketServerEndpoint {private Session session;private String userId;@OnOpenpublic void OnOpen(Session session, @PathParam(value = "userId") String userId) {this.session = session;this.userId = userId;// 建立連接后,將連接存到一個map里endpointMap.put(userId, this);Message message = new Message(0, "connected, hello " + userId);sendMsg(message);}@OnClosepublic void OnClose() {// 關閉連接時觸發,從map中刪除連接endpointMap.remove(userId);System.out.println("server closed...");}@OnMessagepublic void onMessage(Message message) {System.out.println("server recive message=" + message.toString());}@OnErrorpublic void onError(Throwable t) throws Throwable {this.session.close(new CloseReason(CloseReason.CloseCodes.CLOSED_ABNORMALLY, "系統異常"));t.printStackTrace();}/*** 群發* @param data*/public void sendAllMsg(Message data) {for (WebSocketServerEndpoint value : endpointMap.values()) {value.sendMsgAsync(data);}}/*** 推送消息給指定 userId* @param data* @param userId*/public void sendMsg(Message data, String userId) {WebSocketServerEndpoint endpoint = endpointMap.get(userId);if (endpoint == null) {System.out.println("not conected to " + userId);return;}endpoint.sendMsgAsync(data);}private void sendMsg(Message data) {try {this.session.getBasicRemote().sendObject(data);} catch (IOException ioException) {ioException.printStackTrace();} catch (EncodeException e) {e.printStackTrace();}}private void sendMsgAsync(Message data) {this.session.getAsyncRemote().sendObject(data);}// 存儲建立連接的Endpointprivate static ConcurrentHashMap<String, WebSocketServerEndpoint> endpointMap = new ConcurrentHashMap<String, WebSocketServerEndpoint>(); }

每一個客戶端與服務器端建立連接后,都會生成一個WebSocketServerEndpoint,可以通過一個Map將其與userId對應存起來,為后續群發廣播和單獨推送消息給某個客戶端提供便利。

注意:@ServerEndpoint的encoders、decoders、configurator等配置信息在實際使用中可以不定義,如果項目簡單,完全可以用默認的。

如果通信消息被封裝成一個對象,如示例的Message(因為源碼過于簡單就不展示了,屬性主要有code、msg、data),就必須提供編碼器和解碼器。也可以在每次發送消息時硬編碼轉為字符串,在接收到消息時轉為Message。有了編碼器和解碼器,顯得比較規范,轉為字符串由編碼器做,字符串轉為對象由解碼器做,但也使得架構變復雜了,視項目需求而定。

Configurator的用處就是自定義Endpoint對象創建方式,默認Tomcat提供的是通過反射。WebScoket是每個連接都會創建一個Endpoint對象,如果連接比較多,很頻繁,通過反射創建,用后即毀,可能不是一個好主意,所以可以搞一個對象池,用過回收,用時先從對象池中拿,有就重置,省去實例化分配內存等消耗過程。

如果使用SpringBoot內置Tomcat、undertow、Netty等,接入WebSocket時除了加@ServerEndpoint還需要加一個@Component,再給Spring注冊一個ServerEndpointExporter類,這樣,服務端Endpoint就交由Spring去掃描注冊了。

@Configuration public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {ServerEndpointExporter serverEndpointExporter = new ServerEndpointExporter();return serverEndpointExporter;} }

外置Tomcat就不需要這么麻煩,Tomcat會默認掃描classpath下帶有@ServerEndpoint注解的類。(SpringBoot接入Websocket后續會單獨出文章講解,也挺有意思的)

(2)繼承抽象類Endpoint方式

import javax.websocket.*; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap;public class WebSocketServerEndpoint extends Endpoint {private Session session;private String userId;@Overridepublic void onOpen(Session session, EndpointConfig endpointConfig) {this.session = session;this.userId = session.getPathParameters().get("userId");session.addMessageHandler(new MessageHandler());endpointMap.put(userId, this);Message message = new Message(0, "connected, hello " + userId);sendMsg(message);}@Overridepublic void onClose(Session session, CloseReason closeReason) {endpointMap.remove(userId);}@Overridepublic void onError(Session session, Throwable throwable) {throwable.printStackTrace();}/*** 群發* @param data*/public void sendAllMsg(Message data) {for (WebSocketServerEndpoint value : endpointMap.values()) {value.sendMsgAsync(data);}}/*** 推送消息給指定 userId* @param data* @param userId*/public void sendMsg(Message data, String userId) {WebSocketServerEndpoint endpoint = endpointMap.get(userId);if (endpoint == null) {System.out.println("not conected to " + userId);return;}endpoint.sendMsgAsync(data);}private void sendMsg(Message data) {try {this.session.getBasicRemote().sendObject(data);} catch (IOException ioException) {ioException.printStackTrace();} catch (EncodeException e) {e.printStackTrace();}}private void sendMsgAsync(Message data) {this.session.getAsyncRemote().sendObject(data);}private class MessageHandler implements javax.websocket.MessageHandler.Whole<Message> {@Overridepublic void onMessage(Message message) {System.out.println("server recive message=" + message.toString());}}private static ConcurrentHashMap<String, WebSocketServerEndpoint> endpointMap = new ConcurrentHashMap<String, WebSocketServerEndpoint>();}

繼承抽象類Endpoint方式比加注解@ServerEndpoint方式麻煩的很,主要是需要自己實現MessageHandler和ServerApplicationConfig。@ServerEndpoint的話都是使用默認的,原理上差不多,只是注解更自動化,更簡潔。

MessageHandler做的事情,一個@OnMessage就搞定了,ServerApplicationConfig做的URI映射、decoders、encoders,configurator等,一個@ServerEndpoint就可以了。

import javax.websocket.Decoder; import javax.websocket.Encoder; import javax.websocket.Endpoint; import javax.websocket.server.ServerApplicationConfig; import javax.websocket.server.ServerEndpointConfig; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set;public class MyServerApplicationConfig implements ServerApplicationConfig {@Overridepublic Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> set) {Set<ServerEndpointConfig> result = new HashSet<ServerEndpointConfig>();List<Class<? extends Decoder>> decoderList = new ArrayList<Class<? extends Decoder>>();decoderList.add(MessageDecoder.class);List<Class<? extends Encoder>> encoderList = new ArrayList<Class<? extends Encoder>>();encoderList.add(MessageEncoder.class);if (set.contains(WebSocketServerEndpoint3.class)) {ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketServerEndpoint3.class, "/ws/test3").decoders(decoderList).encoders(encoderList).configurator(new MyServerConfigurator()).build();result.add(serverEndpointConfig);}return result;}@Overridepublic Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> set) {return set;} }

如果使用SpringBoot內置Tomcat,則不需要ServerApplicationConfig了,但是需要給Spring注冊一個ServerEndpointConfig。

@Bean public ServerEndpointConfig serverEndpointConfig() {List<Class<? extends Decoder>> decoderList = new ArrayList<Class<? extends Decoder>>();decoderList.add(MessageDecoder.class);List<Class<? extends Encoder>> encoderList = new ArrayList<Class<? extends Encoder>>();encoderList.add(MessageEncoder.class);ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketServerEndpoint3.class, "/ws/test3/{userId}").decoders(decoderList).encoders(encoderList).configurator(new MyServerConfigurator()).build();return serverEndpointConfig; }

(3)早期Tomcat7中Server端實現對比

Tomcat7早期版本7.0.47之前還沒有出JSR 356時,自己搞了一套接口,其實就是一個Servlet。

和遵循JSR356標準的版本對比,有一個比較大的變化是,createWebSocketInbound創建生命周期事件處理器StreamInbound的時機是WebSocket協議升級之前,此時還可以通過用戶線程緩存(ThreadLocal等)的HttpServletRequest對象,獲取一些請求頭等信息。

而遵循JSR356標準的版本實現,創建生命周期事件處理的Endpoint是在WebSocket協議升級完成(經過HTTP握手)之后創建的,而WebSocket握手成功給客戶端響應101前,會結束銷毀HttpServletRequest對象,此時是獲取不到請求頭等信息的。

import org.apache.catalina.websocket.StreamInbound; import org.apache.catalina.websocket.WebSocketServlet;import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest;@WebServlet(urlPatterns = "/ws/test") public class MyWeSocketServlet extends WebSocketServlet {@Overrideprotected StreamInbound createWebSocketInbound(String subProtocol, HttpServletRequest request) {MyMessageInbound messageInbound = new MyMessageInbound(subProtocol, request);return messageInbound;}} import org.apache.catalina.websocket.MessageInbound; import org.apache.catalina.websocket.WsOutbound;import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer;public class MyMessageInbound extends MessageInbound {private String subProtocol;private HttpServletRequest request;public MyMessageInbound(String subProtocol, HttpServletRequest request) {this.subProtocol = subProtocol;this.request = request;}@Overrideprotected void onOpen(WsOutbound outbound) {String msg = "connected, hello";ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());try {outbound.writeBinaryMessage(byteBuffer);} catch (IOException e) {e.printStackTrace();}}@Overrideprotected void onClose(int status) {}@Overrideprotected void onBinaryMessage(ByteBuffer byteBuffer) throws IOException {// 接收到客戶端信息}@Overrideprotected void onTextMessage(CharBuffer charBuffer) throws IOException {// 接收到客戶端信息} }

2、客戶端實現

(1)前端js版

js版的客戶端主要依托瀏覽器對WebScoket的支持,在生命周期事件觸發上和服務器端的差不多,這也應證了建立WebSocket連接的兩端是對等的。

編寫WebSocket客戶端需要注意以下幾點:

  • 和服務器端商議好傳輸的消息的格式,一般為json字符串,比較直觀,編碼解碼都很簡單,也可以是其他商定的格式。
  • 需要心跳檢測,定時給服務器端發送消息,保持連接正常。
  • 正常關閉連接,即關閉瀏覽器窗口前主動關閉連接,以免服務器端拋異常。
  • 如果因為異常斷開連接,支持重連。
// 對websocket進行簡單封裝 WebSocketOption.prototype = {// 創建websocket操作createWebSocket: function () {try {if('WebSocket' in window) {this.ws = new WebSocket(this.wsUrl);} else if('MozWebSocket' in window) { this.ws = new MozWebSocket(this.wsUrl);} else {alert("您的瀏覽器不支持websocket協議,建議使用新版谷歌、火狐等瀏覽器,請勿使用IE10以下瀏覽器,360瀏覽器請使用極速模式,不要使用兼容模式!"); }this.lifeEventHandle();} catch(e) {this.reconnect(this.wsUrl);console.log(e);} },// 生命周期事件操作lifeEventHandle: function() {var self = this;this.ws.onopen = function (event) {self.connectCount = 1;//心跳檢測重置if (self.heartCheck == null) {self.heartCheck = new HeartCheckObj(self.ws);}self.sendMsg(5, "")self.heartCheck.reset().start(); console.log("websocket連接成功!" + new Date().toUTCString());};this.ws.onclose = function (event) {// 全部設置為初始值self.heartCheck = null;self.reconnect(self.wsUrl); console.log("websocket連接關閉!" + new Date().toUTCString());};this.ws.onerror = function () {self.reconnect(self.wsUrl);console.log("websocket連接錯誤!");};//如果獲取到消息,心跳檢測重置this.ws.onmessage = function (event) { //心跳檢測重置if (self.heartCheck == null) {self.heartCheck = new HeartCheckObj(self.ws);}self.heartCheck.reset().start(); console.log("websocket收到消息啦:" + event.data);// 業務處理// 接收到的消息可以放到localStorage里,然后在其他地方取出來}},// 斷線重連操作reconnect: function() {var self = this;if (this.lockReconnect) return;console.log(this.lockReconnect)this.lockReconnect = true;//沒連接上會一直重連,設置延遲避免請求過多,重連時間設置按倍數增加setTimeout(function () { self.createWebSocket(self.wsUrl);self.lockReconnect = false;self.connectCount++;}, 10000 * (self.connectCount));},// 發送消息操作sendMsg: function(cmd, data) {var sendData = {"cmd": cmd, "msg": data};try {this.ws.send(JSON.stringify(sendData));} catch(err) {console.log("發送數據失敗, err=" + err)}},// 關閉websocket接口操作closeWs: function() {this.ws.close();} }/*** 封裝心跳檢測對象<p>*/ function HeartCheckObj(ws) {this.ws = ws;// 心跳時間this.timeout = 10000;// 定時事件this.timeoutObj = null;// 自動斷開事件this.serverTimeoutObj = null; } HeartCheckObj.prototype = {setWs: function(ws) {this.ws = ws;},reset: function() {clearTimeout(this.timeoutObj);clearTimeout(this.serverTimeoutObj);return this;},// 開始心跳檢測start: function() {var self = this;this.timeoutObj = setTimeout(function() {//這里發送一個心跳,后端收到后,返回一個心跳消息,//onmessage拿到返回的心跳就說明連接正常var ping = {"cmd":1, "msg": "ping"};self.ws.send(JSON.stringify(ping));//如果onmessage那里超過一定時間還沒重置,說明后端主動斷開了self.serverTimeoutObj = setTimeout(function() {//如果onclose會執行reconnect,我們執行ws.close()就行了.如果直接執行reconnect 會觸發onclose導致重連兩次self.ws.close(); }, self.timeout)}, self.timeout)} }/*** -------------------------* 創建websocket的主流程 ** -------------------------*/ var currentDomain = document.domain; var wsUrl = "ws://" + currentDomain + "/test"var webSocketOption = new WebSocketOption(wsUrl) webSocketOption.createWebSocket()// 監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。 window.onbeforeunload = function() {webSocketOption.closeWs(); }

這里推薦一個在線測試WebSocket連接和發送消息的網站easyswoole.com/wstool.html:

真的很牛逼,很方便,很簡單。還有源碼github:https://github.com/easy-swoole/wstool,感興趣可以看看。

(2)@ClientEndpoint注解方式

Java版客戶端不用多說,把@ServerEndpoint換成@ClientEndpoint就可以了,其他都一樣。@ClientEndpoint比@ServerEndpoint就少了一個value,不需要設置URI。

@ClientEndpoint(encoders = {MessageEncoder.class}, decoders = {MessageDecoder.class}) public class WebSocketClientEndpoint {private Session session;@OnOpenpublic void OnOpen(Session session) {this.session = session;Message message = new Message(0, "connecting...");sendMsg(message);}@OnClosepublic void OnClose() {Message message = new Message(0, "client closed...");sendMsg(message);System.out.println("client closed");}@OnMessagepublic void onMessage(Message message) {System.out.println("client recive message=" + message.toString());}@OnErrorpublic void onError(Throwable t) throws Throwable {t.printStackTrace();}public void sendMsg(Message data) {try {this.session.getBasicRemote().sendObject(data);} catch (IOException ioException) {ioException.printStackTrace();} catch (EncodeException e) {e.printStackTrace();}}public void sendMsgAsync(Message data) {this.session.getAsyncRemote().sendObject(data);} }

連接服務器端:

WebSocketContainer container = ContainerProvider.getWebSocketContainer(); container.connectToServer(WebSocketClientEndpoint.class,new URI("ws://localhost:8080/ws/test"));

(3)繼承抽象類Endpoint方式

繼承抽象類Endpoint方式也和服務器端的差不多,但是不需要實現ServerApplicationConfig,需要實例化一個ClientEndpointConfig。Endpoint實現類和服務器端的一樣,就省略了,如下是連接服務器端的代碼:

ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); container.connectToServer(new WebSocketClientEndpoint(),clientEndpointConfig,new URI("ws://localhost:8080/websocket/hello"));

3、基于Nginx反向代理注意事項

一般web服務器會用Nginx做反向代理,經過Nginx反向轉發的HTTP請求不會帶上Upgrade和Connection消息頭,所以需要在Nginx配置里顯式指定需要升級為WebSocket的URI帶上這兩個頭:

location /chat/ {proxy_pass http://backend;proxy_http_version 1.1;proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "upgrade";proxy_connect_timeout 4s; proxy_read_timeout 7200s; proxy_send_timeout 12s; }

默認情況下,如果代理服務器在60秒內沒有傳輸任何數據,連接將被關閉。這個超時可以通過proxy_read_timeout指令來增加。或者,可以將代理服務器配置為定期發送WebSocket PING幀以重置超時并檢查連接是否仍然活躍。

具體可參考:http://nginx.org/en/docs/http/websocket.html

五、WebSocket在Tomcat中的源碼實現

所有兼容Java EE的應用服務器,必須遵循JSR356 WebSocket Java API標準,Tomcat也不例外。而且Tomcat也是支持WebSocket最早的Web應用服務器框架(之一),在還沒有出JSR356標準時,就已經自定義了一套WebSocket API,但是JSR356一出,不得不改弦更張。

通過前面的講解,在使用上完全沒有問題,但是有幾個問題完全是黑盒的:

  • Server Endpoint 是如何被掃描加載的?
  • WebSocket是如何借助HTTP 進行握手升級的?
  • WebSocket建立連接后如何保持連接不斷,互相通信的?

(如下源碼解析,需要對Tomcat連接器源碼有一定了解)

1、WsSci初始化

Tomcat 提供了一個org.apache.tomcat.websocket.server.WsSci類來初始化、加載WebSocket。從類名上顧名思義,利用了Sci加載機制,何為Sci加載機制?就是實現接口 jakarta.servlet.ServletContainerInitializer,在Tomcat部署裝載Web項目(org.apache.catalina.core.StandardContext#startInternal)時主動觸發ServletContainerInitializer#onStartup,做一些擴展的初始化操作。

WsSci主要做了一件事,就是掃描加載Server Endpoint,并將其加到WebSocket容器里jakarta.websocket.WebSocketContainer。

WsSci主要會掃描三種類:

  • 加了@ServerEndpoint的類。
  • Endpoint的子類。
  • ServerApplicationConfig的子類。

(1)WsSci#onStartup

@HandlesTypes({ServerEndpoint.class, ServerApplicationConfig.class,Endpoint.class}) public class WsSci implements ServletContainerInitializer {@Overridepublic void onStartup(Set<Class<?>> clazzes, ServletContext ctx)throws ServletException {WsServerContainer sc = init(ctx, true);if (clazzes == null || clazzes.size() == 0) {return;}// Group the discovered classes by typeSet<ServerApplicationConfig> serverApplicationConfigs = new HashSet<>();Set<Class<? extends Endpoint>> scannedEndpointClazzes = new HashSet<>();Set<Class<?>> scannedPojoEndpoints = new HashSet<>();try {// wsPackage is "jakarta.websocket."String wsPackage = ContainerProvider.class.getName();wsPackage = wsPackage.substring(0, wsPackage.lastIndexOf('.') + 1);for (Class<?> clazz : clazzes) {JreCompat jreCompat = JreCompat.getInstance();int modifiers = clazz.getModifiers();if (!Modifier.isPublic(modifiers) ||Modifier.isAbstract(modifiers) ||Modifier.isInterface(modifiers) ||!jreCompat.isExported(clazz)) {// Non-public, abstract, interface or not in an exported// package (Java 9+) - skip it.continue;}// Protect against scanning the WebSocket API JARs// 防止掃描WebSocket API jarif (clazz.getName().startsWith(wsPackage)) {continue;}if (ServerApplicationConfig.class.isAssignableFrom(clazz)) {// 1、clazz是ServerApplicationConfig子類serverApplicationConfigs.add((ServerApplicationConfig) clazz.getConstructor().newInstance());}if (Endpoint.class.isAssignableFrom(clazz)) {// 2、clazz是Endpoint子類@SuppressWarnings("unchecked")Class<? extends Endpoint> endpoint =(Class<? extends Endpoint>) clazz;scannedEndpointClazzes.add(endpoint);}if (clazz.isAnnotationPresent(ServerEndpoint.class)) {// 3、clazz是加了注解ServerEndpoint的類scannedPojoEndpoints.add(clazz);}}} catch (ReflectiveOperationException e) {throw new ServletException(e);}// Filter the resultsSet<ServerEndpointConfig> filteredEndpointConfigs = new HashSet<>();Set<Class<?>> filteredPojoEndpoints = new HashSet<>();if (serverApplicationConfigs.isEmpty()) {// 從這里看出@ServerEndpoint的服務器端是可以不用ServerApplicationConfig的filteredPojoEndpoints.addAll(scannedPojoEndpoints);} else {// serverApplicationConfigs不為空,for (ServerApplicationConfig config : serverApplicationConfigs) {Set<ServerEndpointConfig> configFilteredEndpoints =config.getEndpointConfigs(scannedEndpointClazzes);if (configFilteredEndpoints != null) {filteredEndpointConfigs.addAll(configFilteredEndpoints);}// getAnnotatedEndpointClasses 對于 scannedPojoEndpoints起到一個過濾作用// 不滿足條件的后面不加到WsServerContainer里Set<Class<?>> configFilteredPojos =config.getAnnotatedEndpointClasses(scannedPojoEndpoints);if (configFilteredPojos != null) {filteredPojoEndpoints.addAll(configFilteredPojos);}}}try {// 繼承抽象類Endpoint的需要使用者手動封裝成ServerEndpointConfig// 而加了注解@ServerEndpoint的類 Tomcat會自動封裝成ServerEndpointConfig// Deploy endpointsfor (ServerEndpointConfig config : filteredEndpointConfigs) {sc.addEndpoint(config);}// Deploy POJOsfor (Class<?> clazz : filteredPojoEndpoints) {sc.addEndpoint(clazz, true);}} catch (DeploymentException e) {throw new ServletException(e);}}static WsServerContainer init(ServletContext servletContext,boolean initBySciMechanism) {WsServerContainer sc = new WsServerContainer(servletContext);servletContext.setAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE, sc);// 注冊監聽器WsSessionListener給servletContext,// 在http session銷毀時觸發 ws session的關閉銷毀servletContext.addListener(new WsSessionListener(sc));// Can't register the ContextListener again if the ContextListener is// calling this methodif (initBySciMechanism) {// 注冊監聽器WsContextListener給servletContext,// 在 servletContext初始化時觸發WsSci.init// 在 servletContext銷毀時觸發WsServerContainer的銷毀// 不過呢,只在WsSci.onStartup時注冊一次servletContext.addListener(new WsContextListener());}return sc;} }

從上述源碼中可以看出ServerApplicationConfig起到一個過濾的作用:

  • 當沒有ServerApplicationConfig時,加了@ServerEndpoint的類會默認全部加到一個Set集合(filteredPojoEndpoints),所以加了@ServerEndpoint的類可以不需要自定義實現ServerApplicationConfig。
  • 當有ServerApplicationConfig時,ServerApplicationConfig#getEndpointConfigs用來過濾Endpoint子類,并且Endpoint子類必須封裝成一個ServerEndpointConfig。
  • ServerApplicationConfig#getAnnotatedEndpointClasses用來過濾加了注解@ServerEndpoint的類,一般空實現就行了(如果不想某個類被加到WsServerContainer里,那不加@ServerEndpoint不就可以了)。

過濾之后的Endpoint子類和加了注解@ServerEndpoint的類會分別調用不同形參的WsServerContainer#addEndpoint,將其加到WsServerContainer里。

(2)WsServerContainer#addEndpoint

  • 將Endpoint子類加到WsServerContainer里,調用的是形參為ServerEndpointConfig的addEndpoint:
public void addEndpoint(ServerEndpointConfig sec) throws DeploymentException {addEndpoint(sec, false); }

因為Endpoint子類需要使用者封裝成ServerEndpointConfig,不需要Tomcat來封裝。

  • 將加了注解@ServerEndpoint的類加到WsServerContainer,調用的是形參為Class<?>的addEndpoint(fromAnnotatedPojo參數暫時在這個方法里沒什么用處):

該方法主要職責就是解析@ServerEndpoint,獲取path、decoders、encoders、configurator等構建一個ServerEndpointConfig對象

最終調用的都是如下這個比較復雜的方法,fromAnnotatedPojo表示是否是加了@ServerEndpoint的類。主要做了兩件事:

  • 對加了@ServerEndpoint類的生命周期方法(@OnOpen、@OnClose、@OnError、@OnMessage)的掃描和映射封裝。

  • 對path的有效性檢查和path param解析。

(3)PojoMethodMapping方法映射和形參解析

PojoMethodMapping構造函數比較長,主要是對加了@OnOpen、@OnClose、@OnError、@OnMessage的方法進行校驗和映射,以及對每個方法的形參進行解析和校驗,主要邏輯總結如下:

  • 對當前類以及其父類中的方法進行掃描。
  • 當前類中不能存在多個相同注解的方法,否則會拋出Duplicate annotation異常。
  • 父類和子類中存在相同注解的方法,子類必須重寫該方法,否則會拋出Duplicate annotation異常。
  • 對于@OnMessage,可以有多個,但是接收消息的類型必須不同,消息類型大概分為三種:PongMessage心跳消息、字節型、字符型。
  • 如果掃描到對的注解都是父類的方法,子類重寫了該方法,但是沒有加響應的注解,則會被清除。
  • 形參解析。
public PojoMethodMapping(Class<?> clazzPojo, List<Class<? extends Decoder>> decoderClazzes, String wsPath,InstanceManager instanceManager) throws DeploymentException {this.wsPath = wsPath;List<DecoderEntry> decoders = Util.getDecoders(decoderClazzes, instanceManager);Method open = null;Method close = null;Method error = null;Method[] clazzPojoMethods = null;Class<?> currentClazz = clazzPojo;while (!currentClazz.equals(Object.class)) {Method[] currentClazzMethods = currentClazz.getDeclaredMethods();if (currentClazz == clazzPojo) {clazzPojoMethods = currentClazzMethods;}for (Method method : currentClazzMethods) {if (method.isSynthetic()) {// Skip all synthetic methods.// They may have copies of annotations from methods we are// interested in and they will use the wrong parameter type// (they always use Object) so we can't used them here.continue;}if (method.getAnnotation(OnOpen.class) != null) {checkPublic(method);if (open == null) {open = method;} else {if (currentClazz == clazzPojo ||!isMethodOverride(open, method)) {// Duplicate annotation// 拋出Duplicate annotation異常的兩種情況:// 1. 當前的類有多個相同注解的方法,如有兩個@OnOpen// 2. 當前類時父類,有相同注解的方法,但是其子類沒有重寫這個方法// 即 父類和子類有多個相同注解的方法,且沒有重寫關系throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateAnnotation",OnOpen.class, currentClazz));}}} else if (method.getAnnotation(OnClose.class) != null) {checkPublic(method);if (close == null) {close = method;} else {if (currentClazz == clazzPojo ||!isMethodOverride(close, method)) {// Duplicate annotationthrow new DeploymentException(sm.getString("pojoMethodMapping.duplicateAnnotation",OnClose.class, currentClazz));}}} else if (method.getAnnotation(OnError.class) != null) {checkPublic(method);if (error == null) {error = method;} else {if (currentClazz == clazzPojo ||!isMethodOverride(error, method)) {// Duplicate annotationthrow new DeploymentException(sm.getString("pojoMethodMapping.duplicateAnnotation",OnError.class, currentClazz));}}} else if (method.getAnnotation(OnMessage.class) != null) {checkPublic(method);MessageHandlerInfo messageHandler = new MessageHandlerInfo(method, decoders);boolean found = false;// 第一次掃描OnMessage時,onMessage為空,不會走下面的for,然后就把messageHandler加到onMessage里// 如果非首次掃描到這里,即向上掃描父類,允許有多個接收消息類型完全不同的onmessagefor (MessageHandlerInfo otherMessageHandler : onMessage) {// 如果多個onmessage接收的消息類型有相同的,則可能會拋出Duplicate annotation// 1. 同一個類中多個onmessage有接收相同類型的消息// 2. 父子類中多個onmessage有接收相同類型的消息,但不是重寫關系if (messageHandler.targetsSameWebSocketMessageType(otherMessageHandler)) {found = true;if (currentClazz == clazzPojo ||!isMethodOverride(messageHandler.m, otherMessageHandler.m)) {// Duplicate annotationthrow new DeploymentException(sm.getString("pojoMethodMapping.duplicateAnnotation",OnMessage.class, currentClazz));}}}if (!found) {onMessage.add(messageHandler);}} else {// Method not annotated}}currentClazz = currentClazz.getSuperclass();}// If the methods are not on clazzPojo and they are overridden// by a non annotated method in clazzPojo, they should be ignoredif (open != null && open.getDeclaringClass() != clazzPojo) {// open 有可能是父類的,子類即clazzPojo有重寫該方法,但是沒有加OnOpen注解// 則 open置為nullif (isOverridenWithoutAnnotation(clazzPojoMethods, open, OnOpen.class)) {open = null;}}if (close != null && close.getDeclaringClass() != clazzPojo) {if (isOverridenWithoutAnnotation(clazzPojoMethods, close, OnClose.class)) {close = null;}}if (error != null && error.getDeclaringClass() != clazzPojo) {if (isOverridenWithoutAnnotation(clazzPojoMethods, error, OnError.class)) {error = null;}}List<MessageHandlerInfo> overriddenOnMessage = new ArrayList<>();for (MessageHandlerInfo messageHandler : onMessage) {if (messageHandler.m.getDeclaringClass() != clazzPojo&& isOverridenWithoutAnnotation(clazzPojoMethods, messageHandler.m, OnMessage.class)) {overriddenOnMessage.add(messageHandler);}}// 子類重寫了的onmessage方法,但沒有加OnMessage注解的需要從onMessage list 中刪除for (MessageHandlerInfo messageHandler : overriddenOnMessage) {onMessage.remove(messageHandler);}this.onOpen = open;this.onClose = close;this.onError = error;// 參數解析onOpenParams = getPathParams(onOpen, MethodType.ON_OPEN);onCloseParams = getPathParams(onClose, MethodType.ON_CLOSE);onErrorParams = getPathParams(onError, MethodType.ON_ERROR); }

雖然方法名可以隨意,但是形參卻有著強制限制:

  • @onOpen方法,可以有的參數Session、EndpointConfig、@PathParam,不能有其他參數。
  • @onError方法,可以有的參數Session、@PathParam, 必須有Throwable,不能有其他參數。
  • @onClose方法,可以有的參數Session, CloseReason, @PathParam,不能有其他參數。

2、協議升級(握手)

Tomcat中WebSocket是通過UpgradeToken機制實現的,其具體的升級處理器為WsHttpUpgradeHandler。WebSocket協議升級的過程比較曲折,首先要通過過濾器WsFilter進行升級判斷,然后調用org.apache.catalina.connector.Request#upgrade進行UpgradeToken的構建,最后通過org.apache.catalina.connector.Request#coyoteRequest回調函數action將UpgradeToken回傳給連接器為后續升級處理做準備。

(1)WsFilter

WebSocket協議升級的過程比較曲折。帶有WebSocket握手的請求會平安經過Tomcat的Connector,被轉發到Servlet容器中,在業務處理之前經過過濾器WsFilter判斷是否需要升級(WsFilter 在 org.apache.catalina.core.ApplicationFilterChain過濾鏈中觸發):

  • 首先判斷WsServerContainer是否有進行Endpoint的掃描和注冊以及請頭中是否有Upgrade: websocket。
  • 獲取請求path即uri在WsServerContainer中找對應的ServerEndpointConfig。
  • 調用UpgradeUtil.doUpgrade進行升級。

(2)UpgradeUtil#doUpgrade

UpgradeUtil#doUpgrade主要做了如下幾件事情:

  • 檢查HttpServletRequest的一些請求頭的有效性,如Connection: upgrade、Sec-WebSocket-Version:13、Sec-WebSocket-Key等。
  • 給HttpServletResponse設置一些響應頭,如Upgrade:websocket、Connection: upgrade、根據Sec-WebSocket-Key的值生成響應頭Sec-WebSocket-Accept的值。
  • 封裝WsHandshakeRequest和WsHandshakeResponse。
  • 調用HttpServletRequest#upgrade進行升級,并獲取WsHttpUpgradeHandler(具體的升級流程處理器)。
// org.apache.tomcat.websocket.server.UpgradeUtil#doUpgrade public static void doUpgrade(WsServerContainer sc, HttpServletRequest req,HttpServletResponse resp, ServerEndpointConfig sec,Map<String,String> pathParams)throws ServletException, IOException {// Validate the rest of the headers and reject the request if that// validation failsString key;String subProtocol = null;// 檢查請求頭中是否有 Connection: upgradeif (!headerContainsToken(req, Constants.CONNECTION_HEADER_NAME,Constants.CONNECTION_HEADER_VALUE)) {resp.sendError(HttpServletResponse.SC_BAD_REQUEST);return;}// 檢查請求頭中的 Sec-WebSocket-Version:13if (!headerContainsToken(req, Constants.WS_VERSION_HEADER_NAME,Constants.WS_VERSION_HEADER_VALUE)) {resp.setStatus(426);resp.setHeader(Constants.WS_VERSION_HEADER_NAME,Constants.WS_VERSION_HEADER_VALUE);return;}// 獲取 Sec-WebSocket-Keykey = req.getHeader(Constants.WS_KEY_HEADER_NAME);if (key == null) {resp.sendError(HttpServletResponse.SC_BAD_REQUEST);return;}// Origin check,校驗 Origin 是否有權限String origin = req.getHeader(Constants.ORIGIN_HEADER_NAME);if (!sec.getConfigurator().checkOrigin(origin)) {resp.sendError(HttpServletResponse.SC_FORBIDDEN);return;}// Sub-protocolsList<String> subProtocols = getTokensFromHeader(req,Constants.WS_PROTOCOL_HEADER_NAME);subProtocol = sec.getConfigurator().getNegotiatedSubprotocol(sec.getSubprotocols(), subProtocols);// Extensions// Should normally only be one header but handle the case of multiple// headersList<Extension> extensionsRequested = new ArrayList<>();Enumeration<String> extHeaders = req.getHeaders(Constants.WS_EXTENSIONS_HEADER_NAME);while (extHeaders.hasMoreElements()) {Util.parseExtensionHeader(extensionsRequested, extHeaders.nextElement());}// Negotiation phase 1. By default this simply filters out the// extensions that the server does not support but applications could// use a custom configurator to do more than this.List<Extension> installedExtensions = null;if (sec.getExtensions().size() == 0) {installedExtensions = Constants.INSTALLED_EXTENSIONS;} else {installedExtensions = new ArrayList<>();installedExtensions.addAll(sec.getExtensions());installedExtensions.addAll(Constants.INSTALLED_EXTENSIONS);}List<Extension> negotiatedExtensionsPhase1 = sec.getConfigurator().getNegotiatedExtensions(installedExtensions, extensionsRequested);// Negotiation phase 2. Create the Transformations that will be applied// to this connection. Note than an extension may be dropped at this// point if the client has requested a configuration that the server is// unable to support.List<Transformation> transformations = createTransformations(negotiatedExtensionsPhase1);List<Extension> negotiatedExtensionsPhase2;if (transformations.isEmpty()) {negotiatedExtensionsPhase2 = Collections.emptyList();} else {negotiatedExtensionsPhase2 = new ArrayList<>(transformations.size());for (Transformation t : transformations) {negotiatedExtensionsPhase2.add(t.getExtensionResponse());}}// Build the transformation pipelineTransformation transformation = null;StringBuilder responseHeaderExtensions = new StringBuilder();boolean first = true;for (Transformation t : transformations) {if (first) {first = false;} else {responseHeaderExtensions.append(',');}append(responseHeaderExtensions, t.getExtensionResponse());if (transformation == null) {transformation = t;} else {transformation.setNext(t);}}// Now we have the full pipeline, validate the use of the RSV bits.if (transformation != null && !transformation.validateRsvBits(0)) {throw new ServletException(sm.getString("upgradeUtil.incompatibleRsv"));}// 設置resp的響應頭Upgrade:websocket、 Connection: upgrade 、Sec-WebSocket-Accept:// If we got this far, all is good. Accept the connection.resp.setHeader(Constants.UPGRADE_HEADER_NAME,Constants.UPGRADE_HEADER_VALUE);resp.setHeader(Constants.CONNECTION_HEADER_NAME,Constants.CONNECTION_HEADER_VALUE);// 通過Sec-WebSocket-Key生成Sec-WebSocket-Accept的值resp.setHeader(HandshakeResponse.SEC_WEBSOCKET_ACCEPT,getWebSocketAccept(key));if (subProtocol != null && subProtocol.length() > 0) {// RFC6455 4.2.2 explicitly states "" is not valid hereresp.setHeader(Constants.WS_PROTOCOL_HEADER_NAME, subProtocol);}if (!transformations.isEmpty()) {resp.setHeader(Constants.WS_EXTENSIONS_HEADER_NAME, responseHeaderExtensions.toString());}WsHandshakeRequest wsRequest = new WsHandshakeRequest(req, pathParams);WsHandshakeResponse wsResponse = new WsHandshakeResponse();WsPerSessionServerEndpointConfig perSessionServerEndpointConfig =new WsPerSessionServerEndpointConfig(sec);sec.getConfigurator().modifyHandshake(perSessionServerEndpointConfig,wsRequest, wsResponse);wsRequest.finished();// Add any additional headersfor (Entry<String,List<String>> entry :wsResponse.getHeaders().entrySet()) {for (String headerValue: entry.getValue()) {resp.addHeader(entry.getKey(), headerValue);}}// 調用 request.upgrade 進行升級WsHttpUpgradeHandler wsHandler =req.upgrade(WsHttpUpgradeHandler.class);wsHandler.preInit(perSessionServerEndpointConfig, sc, wsRequest,negotiatedExtensionsPhase2, subProtocol, transformation, pathParams,req.isSecure());}

(3)Request#upgrade

Request#upgrade主要做了三件事:

  • 實例化WsHttpUpgradeHandler并構建UpgradeToken。
  • 回調coyoteRequest.action,將UpgradeToken回傳給連接器。
  • 設置響應碼101。
// org.apache.catalina.connector.Request#upgrade public <T extends HttpUpgradeHandler> T upgrade(Class<T> httpUpgradeHandlerClass) throws java.io.IOException, ServletException {T handler;InstanceManager instanceManager = null;try {// Do not go through the instance manager for internal Tomcat classes since they don't// need injectionif (InternalHttpUpgradeHandler.class.isAssignableFrom(httpUpgradeHandlerClass)) {handler = httpUpgradeHandlerClass.getConstructor().newInstance();} else {instanceManager = getContext().getInstanceManager();handler = (T) instanceManager.newInstance(httpUpgradeHandlerClass);}} catch (ReflectiveOperationException | NamingException | IllegalArgumentException |SecurityException e) {throw new ServletException(e);}// 構建 UpgradeToken,UpgradeToken主要包含WsHttpUpgradeHandler、context、協議名稱protocolUpgradeToken upgradeToken = new UpgradeToken(handler, getContext(), instanceManager,getUpgradeProtocolName(httpUpgradeHandlerClass));// 回調action 進行升級coyoteRequest.action(ActionCode.UPGRADE, upgradeToken);// Output required by RFC2616. Protocol specific headers should have// already been set.// 設置響應101response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS);return handler; }

(4)回調機制ActionHook#action

一些發生在Servlet容器的動作可能需要回傳給連接器做處理,比如WebSocket的握手升級,所以連接器就給org.apache.coyote.Request設置了一個動作鉤子``ActionHook#action。一些動作表示定義在枚舉類ActionCode中,ActionCode.UPGRADE就代表協議升級動作。org.apache.coyote.AbstractProcessor實現了ActionHook接口,ActionCode.UPGRADE動作會調用org.apache.coyote.http11.Http11Processor#doHttpUpgrade,只是簡單將upgradeToken設置給Http11Processor`。

(5)ConnectionHandler#process

Tomcat連接器是同步調用容器業務處理,容器中的業務處理結束后還是回到連接器繼續往下執行。

連接器將請求轉發給容器處理是在適配器里完成的,容器中流程處理結束返回到org.apache.catalina.connector.CoyoteAdapter#service,繼續往下執行,最終結束并回收HttpServletrequest、HttpServletreponse對象。

org.apache.catalina.connector.CoyoteAdapter#service是在org.apache.coyote.http11.Http11Processor#service中調用的,

Http11Processor#service是HTTP請求處理主流程,通過upgradeToken != null來判斷是否為升級操作,s是則返回SocketState.UPGRADING。

最后來到org.apache.coyote.AbstractProtocol.ConnectionHandler#process一個連接處理的主流程,根據Http11Processor#service返回SocketState.UPGRADING來進行升級操作,如下只截取了和WebSocket協議升級相關流程的代碼:

  • 獲取UpgradeToken,從中取出HttpUpgradeHandler,對于WebSocket來說是WsHttpUpgradeHandler。
  • 調用WsHttpUpgradeHandler#init啟動協議升級處理。

(6)WsHttpUpgradeHandler#init握手成功

走到這里,基本上就是握手成功了,接下來就是創建WsSession和觸發onOpen。

WsSession的構建中會實例化Endpoint,如果實例化出來的對象不是Endpoint類型,即加了@ServerEndpoint的實例對象,則用一個PojoEndpointServer進行包裝,而PojoEndpointServer是繼承了抽象類Endpoint的。

觸發onOpen時會將WsSession傳進去,對于加PojoEndpointServer,因為用戶自定義的方法名和形參不確定,所以通過反射調用用戶自定義的onopen形式的方法,并且會將通過@onMessage解析出的MessageHandler設置給WsSession。

3、數據傳輸和解析

握手成功之后就建立了雙向通信的連接,該連接有別于HTTP/1.1長連接(應用服務器中工作線程循環占用),而是占用一條TCP連接。在連接建立是進行TCP三次握手,之后全雙工互相通信,將不需要再進行耗時的TCP的三次握手和四次揮手,一方需要關閉WebSocket連接時,發送關閉幀,另一方接收到關閉幀之后,也發送個關閉幀作為響應,之后就認為WebSocket連接關閉了,并且關閉底層TCP連接(四次揮手)。

實則WebSocket全雙工是建立在TCP的長鏈接上的,TCP長鏈接長時間沒有消息通信,會定時?;?#xff0c;一般WebSocket會通過代理如nginx等進行連接通信,nginx有一個連接超時沒有任何信息傳輸時,會斷開,所以需要WebSocket一端定時發送心跳保活。

(1)接收客戶端消息

客戶端來了消息,由連接器的Poller輪詢監測socket底層是否有數據到來,有數據可讀,則封裝成一個SocketProcessor扔到線程池里處理,org.apache.coyote.http11.upgrade.UpgradeProcessorInternal#dispatch具有處理升級協議連接,org.apache.tomcat.websocket.server.WsHttpUpgradeHandler#upgradeDispatch是專門處理WebSocket連接的處理器。

org.apache.tomcat.websocket.server.WsFrameServer是對服務器端消息幀處理的封裝,包括讀取底層數據,按消息幀格式解析、拼裝出有效載荷數據,觸發onMessage。

因為源碼篇幅較多,只展示具體源碼調用流程:

(2)發送消息給客戶端

一般,客戶端發送WebSocket握手請求,和服務器端建立連接后,服務器端需要將連接(Endpoint+WsSession)保存起來,為后續主動推送消息給客戶端提供方便。

Tomcat提供了可以發送三種數據類型(文本、二進制、Object對象)和兩種發送方式(同步、異步)的發送消息的方法。

  • org.apache.tomcat.websocket.WsRemoteEndpointAsync異步發送。
  • org.apache.tomcat.websocket.WsRemoteEndpointBasic 同步發送。

發送消息也同樣需要按消息幀格式封裝,然后通過socket寫到網絡里即可。

六、要點回顧

WebSocket的出現不是空穴來風,起初在HTTP/1.1基礎上通過輪詢和長連接達到信息實時同步的功能,但是這并沒有跳出HTTP/1.1自身的缺陷。HTTP/1.1明顯的兩個缺陷:消息頭冗長且為文本傳輸,請求響應模式。為此,WebSocket誕生了,跳出HTTP/1.1,建立一個新的真正全雙工通信協議。

不僅僅要會在項目中使用WebSocket,還要知道其通信原理和在應用服務器中的實現原理,很多注意事項都是在查閱了官方資源和源碼之后恍然大悟的。

  • 在Tomcat中使用WebSocket不可以在Endpoint里獲取緩存的HttpServletRequest對象,因為在WebSocket握手之前,HTTP/1.1請求就算結束了(HttpServletRequest對象被回收),建立連接之后就更是獨立于HTTP/1.1了。
  • 建立連接的WebSocket,會生成新的Endpoint和WsSession。
  • 使用內置Tomcat需要注意,WsSci做的事情交給了Spring做。
  • WebSocket全雙工是建立在TCP長連接的基礎之上。
  • … …

七、參考文獻

  • https://datatracker.ietf.org/doc/html/rfc6455(可能需要翻墻)
  • https://www.oracle.com/technical-resources/articles/java/jsr356.html
  • https://medium.com/swlh/websockets-with-spring-part-1-http-and-websocket-36c69df1c2ee(可能需要翻墻)
  • http://nginx.org/en/docs/http/websocket.html
  • https://zh.wikipedia.org/wiki/WebSocket
  • 書籍:《Tomcat架構解析》劉光瑞(Tomcat8.5)11.3.4 Tomcat的WebSocket實現
  • 書籍:《Tomcat內核設計剖析》汪建(Tomcat7)10.6 WebSocket協議的支持
  • 書籍:《圖解HTTP》9.3 使用瀏覽器進行全雙工通信的WebSocket
  • 極客時間:《深入拆解Tomcat & Jetty》李號雙(Tomcat9.x)18.新特性:Tomcat如何支持WebSocket?
  • Tomcat注釋源碼:https://gitee.com/stefanpy/tomcat-source-code-learning
  • 如若文章有錯誤理解,歡迎批評指正,同時非常期待你的留言和點贊。如果覺得有用,不妨點個在看,讓更多人受益。

    總結

    以上是生活随笔為你收集整理的WebSocket通信原理和在Tomcat中实现源码详解(万字爆肝)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    国产精品久久久久三级 | 色噜噜狠狠狠狠色综合久不 | www.com在线观看 | 欧美黄网站| 日韩午夜一级片 | 亚洲精品午夜久久久 | 狠狠干夜夜 | 色偷偷男人的天堂av | 国产在线美女 | 免费在线黄网 | 在线观看视频97 | 在线视频国产区 | 久久99国产综合精品 | 精品国产成人av | 夜添久久精品亚洲国产精品 | 91中文字幕一区 | 91大神免费在线观看 | 久久国产精品影片 | 日日摸日日添日日躁av | 黄色三级视频片 | 成人免费观看完整版电影 | 伊人天天干 | 国产资源中文字幕 | 久久久国产精品成人免费 | 日韩欧美一区二区三区视频 | 日本中文字幕在线观看 | 91亚洲精品久久久中文字幕 | 天天插综合网 | 四虎国产精品成人免费4hu | 国产一区视频免费在线观看 | 成人av在线播放网站 | 久久黄色免费 | 免费试看一区 | 国产成人福利片 | 天天综合91 | 国产精品久久久久aaaa | 国产精品成人一区二区三区吃奶 | 国产精品大全 | 狠狠干在线 | 久久精品99国产国产精 | 蜜桃av久久久亚洲精品 | 欧美性做爰猛烈叫床潮 | 日韩视频免费在线 | 草草草影院 | 亚洲精品久久激情国产片 | 91精选| 精品人妖videos欧美人妖 | 最新动作电影 | 久久久婷| 国产精品一区二区久久精品爱微奶 | 五月天丁香亚洲 | 午夜精品一区二区三区四区 | 18国产精品白浆在线观看免费 | 亚洲人成人在线 | 亚洲精品国产麻豆 | 黄色免费在线视频 | 日韩av偷拍 | 欧美婷婷综合 | 十八岁免进欧美 | 亚洲精品中文字幕在线 | 国产乱对白刺激视频在线观看女王 | 波多野结衣视频在线 | 久久草在线视频国产 | 高潮久久久久久久久 | 久久国产a | 天天弄天天操 | 色综合久久久久久中文网 | 免费a一级 | 亚洲精品久久久蜜桃直播 | 日韩乱理 | 精品国产一区二区三区噜噜噜 | 久要激情网 | 999久久久欧美日韩黑人 | 久久中文字幕导航 | 麻豆免费视频网站 | 国产一区二区高清 | 国产精品久久二区 | 久爱精品在线 | 国产日韩欧美在线一区 | 国产精品一区二区在线播放 | 国产精品视频线看 | 日本91在线 | 亚洲欧洲美洲av | 天堂av在线网站 | 中文字幕一区二区三区四区在线视频 | 人人网人人爽 | 在线视频一区观看 | 天天干夜夜爱 | 国产一区二区在线播放 | 国产在线永久 | 成人一区二区三区中文字幕 | 成人小视频在线播放 | 人人干人人搞 | av网站大全免费 | 最新av免费在线 | 久久国产精品二国产精品中国洋人 | 亚洲乱码国产乱码精品天美传媒 | 热99在线| 三级黄色在线 | 免费大片黄在线 | 中文字幕影片免费在线观看 | 婷婷深爱 | 欧美日韩在线视频一区二区 | 色噜噜在线观看视频 | 色激情五月 | 综合色中色 | 亚洲经典视频 | 99精品在这里 | 欧美日韩中文在线 | 一级片免费在线 | 国产中文欧美日韩在线 | 西西444www | 午夜av片 | 国产精品久久电影网 | 国产老太婆免费交性大片 | 精品国偷自产国产一区 | 国产高清视频在线播放 | 久青草电影 | 久久99视频免费观看 | 亚洲专区免费观看 | 日本久久久久久久久久 | 久久草在线免费 | 香蕉视频在线免费看 | 国产精品美女久久久久久免费 | 99人久久精品视频最新地址 | 国产区欧美 | 日韩亚洲国产精品 | 激情视频在线高清看 | 天天操天天操 | 狠狠狠狠狠狠狠狠干 | 中文字幕中文字幕在线一区 | 午夜美女wwww | 五月花婷婷| 国产中文字幕久久 | 91香蕉视频好色先生 | 久久视频免费观看 | 毛片激情永久免费 | 天天干天天搞天天射 | 国产成人av网址 | 婷婷狠狠操 | 欧美a级在线免费观看 | 日本护士三级少妇三级999 | 久久艹精品 | 日韩激情第一页 | 亚洲 中文 欧美 日韩vr 在线 | 欧美精品xxx | 在线观看中文字幕 | 国产黄网站在线观看 | 99久久婷婷国产一区二区三区 | 中文字幕亚洲综合久久五月天色无吗'' | 国产亚洲精品成人av久久影院 | 中文字幕资源网在线观看 | 91爱爱视频 | 久久九九影视网 | 人成午夜视频 | 国产精品亚州 | 国产自产在线视频 | 中文字幕 国产视频 | 久久精品综合一区 | 亚洲一区网 | 伊人热| 国产精品一区二区久久精品爱微奶 | 国产精品久久网 | 久久黄色免费观看 | 三级黄色大片在线观看 | 美女黄频视频大全 | 色综合久久中文字幕综合网 | 色综合久久精品 | 91综合久久一区二区 | 欧美国产日韩在线观看 | .国产精品成人自产拍在线观看6 | 国产麻豆视频 | 日韩欧美高清一区二区三区 | 久草视频手机在线 | 香蕉在线视频观看 | 亚洲h视频在线 | 日本久久成人 | 久久久国产网站 | 天天操网 | 99精品99| 国产精彩视频一区二区 | 成 人 黄 色 免费播放 | 国产精品视频地址 | 天天干天天想 | www.天天草 | 中文字幕在线看 | 中文字幕中文字幕在线一区 | 婷五月激情 | av一区在线 | 天天射天天干天天 | 久久综合五月婷婷 | 综合久久综合久久 | 国产一区二区三精品久久久无广告 | 精品国产一区二区三区免费 | 亚洲欧美日韩在线一区二区 | 欧美一二三区在线播放 | 亚洲美女在线国产 | 国产精品刺激对白麻豆99 | 欧美孕交vivoestv另类 | 在线免费观看欧美日韩 | 97超碰在线资源 | 五月开心激情 | 国产成视频在线观看 | 人人澡人人爽 | 24小时日本在线www免费的 | 欧美黄色免费 | 久久免费黄色网址 | 另类五月激情 | 国产精品嫩草影院123 | 亚洲成年人在线播放 | 美女网站视频久久 | 亚洲影视九九影院在线观看 | 日韩精品在线看 | 激情在线网址 | 久久久久久电影 | 亚洲毛片在线观看. | 国产一区二区三区久久久 | 狠狠色丁香婷婷综合久小说久 | 美女视频一区二区 | 成人a级网站 | 香蕉视频在线免费 | 亚洲免费婷婷 | 国产精品永久免费在线 | 成人免费大片黄在线播放 | 激情视频免费在线观看 | 日本深夜福利视频 | 日本在线观看中文字幕 | 欧美aa在线| 精品国产aⅴ一区二区三区 在线直播av | 午夜在线国产 | 97在线视频网站 | 在线а√天堂中文官网 | 国产精品成人久久久 | 在线视频1卡二卡三卡 | 色综合色综合色综合 | 激情综合亚洲精品 | 欧美精品一区二区性色 | 日日干夜夜操视频 | 黄色中文字幕 | 日韩在线免费观看视频 | 国产精品久久电影观看 | 青青色影院 | 国产日韩精品一区二区三区在线 | 国产视频二区三区 | 亚洲精品久久久久www | 日韩av手机在线看 | 日韩免费观看一区二区 | 日韩免费成人 | 国产午夜激情视频 | 中文字幕免费观看 | 在线观看视频99 | 婷婷伊人综合亚洲综合网 | 日韩一区二区三区高清在线观看 | 亚洲涩综合 | 色多多视频在线观看 | 色婷婷久久一区二区 | 国产免费视频一区二区裸体 | 欧美在线a视频 | 国产99一区 | 久久,天天综合 | 久久久国产影院 | 蜜桃麻豆www久久囤产精品 | 亚洲精品在线观看中文字幕 | av线上免费观看 | 精品影院| 精品国产伦一区二区三区观看体验 | 国产色在线 | 香蕉影院在线观看 | 国产精品免费久久 | 亚洲 中文 在线 精品 | 免费一级特黄录像 | 免费久久网| 99福利片| 欧美 日韩 性 | 免费看精品久久片 | 午夜精品久久久久久久久久久 | 国产偷国产偷亚洲清高 | 91精彩视频| 久久精品91视频 | 国产在线观看不卡 | 久久国产视频网 | 999在线视频| 免费看久久 | 国产成人精品一区二区在线 | 免费黄色小网站 | 亚洲一区二区三区毛片 | 视频国产一区二区三区 | 日狠狠 | 亚洲黄色av网址 | 亚洲视频久久久久 | 亚洲,国产成人av | 日韩av一区在线观看 | 在线精品视频在线观看高清 | 亚洲一区二区精品 | 九9热这里真品2 | 日韩av免费一区 | 在线免费中文字幕 | 久久综合婷婷 | 婷婷六月中文字幕 | 狠狠狠操 | 国产精品永久 | 日韩有色| 人人澡人人草 | 高清不卡免费视频 | 日本精品视频一区 | 六月丁香激情综合色啪小说 | 午夜国产影院 | 西西44人体做爰大胆视频 | 一区二区三区中文字幕在线观看 | 成人av影视 | 黄色av高清 | 免费成人黄色 | 欧美一二三区在线观看 | 成人在线观看资源 | 日韩精品aaa | .精品久久久麻豆国产精品 亚洲va欧美 | 日韩一级黄色大片 | 91大神电影 | 亚洲精品免费在线观看视频 | 日本丰满少妇免费一区 | 欧美资源在线观看 | 久久综合久色欧美综合狠狠 | 天天射天天射天天 | 精品毛片在线 | 日本护士撒尿xxxx18 | 国产视频在线观看一区 | 在线亚洲日本 | 国产小视频在线播放 | 开心激情五月网 | 人成在线免费视频 | 蜜臀av一区二区 | 精品a视频| 亚洲少妇久久 | 伊人色综合久久天天 | 91精品网站 | 亚洲精品中文字幕视频 | 久久久久久国产精品999 | 亚洲黄色成人 | 国产麻豆果冻传媒在线观看 | 国产精品毛片久久 | 最近中文字幕高清字幕免费mv | 18国产精品白浆在线观看免费 | 999视频在线播放 | 国产一区二区三区免费视频 | av综合 日韩| 激情电影在线观看 | 人人视频网站 | 国产a级免费 | 国产精品久久久久久久av电影 | 久久69av | 国产乱对白刺激视频不卡 | 福利一区二区三区四区 | 韩国在线一区 | 国内精品久久久久影院一蜜桃 | 亚洲精品乱码久久久久久高潮 | 亚洲精品456在线播放乱码 | 免费视频久久久久 | 91精品国产乱码在线观看 | 国产精品福利在线观看 | 亚州日韩中文字幕 | 久久久久久网址 | 99久久这里只有精品 | 国产一区二区视频在线播放 | 国产日韩视频在线播放 | 欧美日韩三级在线观看 | 免费看污污视频的网站 | 天天爽夜夜爽精品视频婷婷 | 中文av不卡 | 蜜臀av免费一区二区三区 | 成人a在线观看高清电影 | 福利视频区| 国产九色在线播放九色 | 天天射,天天干 | 国产1区2| 国产成人在线观看 | 在线电影a | 欧美视频不卡 | 精品一区二区三区在线播放 | 国产亚洲人 | 在线中文字母电影观看 | 国产成人av电影 | 国产成a人亚洲精v品在线观看 | 超碰97人人在线 | 中文字幕乱码电影 | 精品国产一区二区三区日日嗨 | 久久久久国产精品厨房 | 超碰在线最新网址 | 久久久一本精品99久久精品 | 在线观看免费中文字幕 | 欧美成年黄网站色视频 | 天天操导航 | 香蕉91视频| 欧美影片 | 久久 亚洲视频 | 欧美日韩亚洲第一页 | 亚洲成a人片综合在线 | 久久国产精品99久久久久 | 天天干天天操天天操 | 久久国产一区二区 | 草在线| 久久精品一区 | 五月婷婷操 | 成人久久久精品国产乱码一区二区 | 日韩午夜小视频 | 久久久电影网站 | 午夜美女网站 | 日韩精品久久久免费观看夜色 | 久久久午夜精品理论片中文字幕 | 96视频免费在线观看 | 91精品国产91久久久久久三级 | 亚州精品在线视频 | 天天操天天干天天综合网 | 黄网站www| 九九色视频 | 国产三级在线播放 | 婷婷综合激情 | 日韩区视频| 色多多污污在线观看 | 国产精品国产三级在线专区 | 中文国产成人精品久久一 | 日韩特级黄色片 | 欧美日韩啪啪 | 日韩久久久久 | 久久综合成人 | 成片免费观看视频大全 | 999久久国产精品免费观看网站 | 国产黄色免费 | 狠狠色丁香婷婷综合视频 | 欧美伦理电影一区二区 | 探花国产在线 | 久久99精品国产一区二区三区 | 国产免费久久久久 | 国产精品视频大全 | 免费高清国产 | 青草视频在线看 | 91黄站| 91色在线观看视频 | 973理论片235影院9 | 日韩免费中文字幕 | 99精品视频在线观看播放 | 99综合视频 | 九九在线高清精品视频 | 丁香五香天综合情 | 欧美 高跟鞋交 xxxxhd | 91看片淫黄大片在线播放 | 日韩在线第一区 | 久久免费av电影 | 国产精品永久久久久久久www | 国产精品一区二区免费 | 免费看片黄色 | 人人涩| 亚洲视频久久 | 中文字幕在线免费 | 日韩va在线观看 | 激情网站五月天 | 18国产精品福利片久久婷 | 免费在线看成人av | 国产探花 | av 一区二区三区四区 | 天堂av色婷婷一区二区三区 | 伊甸园av在线 | 中文字幕国产一区二区 | 人人干在线观看 | 国产一区二区精 | 成人作爱视频 | 香蕉在线观看视频 | 欧美国产精品久久久久久免费 | 免费精品在线 | bbbbb女女女女女bbbbb国产 | 2019中文在线观看 | 日韩天天干 | 国产精品99久久免费黑人 | www.com久久| 欧美激情视频一区二区三区 | 中文字幕在线观看网站 | 日日操操操 | 91看片看淫黄大片 | 日本女人的性生活视频 | 日本中文字幕高清 | 精品嫩模福利一区二区蜜臀 | 久久免费在线观看视频 | 婷婷在线免费 | 日韩av手机在线看 | 丰满少妇在线观看 | 正在播放久久 | 波多野结衣在线观看视频 | 中文乱码视频在线观看 | 国产一区在线播放 | av中文字幕在线观看网站 | 天天鲁一鲁摸一摸爽一爽 | 美女网站视频久久 | 欧美日韩一区二区三区不卡 | 精品国产视频在线 | 国产精品18久久久 | 中文字幕婷婷 | 精品中文字幕在线 | 国产原厂视频在线观看 | 国产流白浆高潮在线观看 | 国产精品手机播放 | 人人精久 | 久久久av电影 | 成人免费中文字幕 | 国产精品永久在线观看 | 最近最新中文字幕 | 久久永久视频 | 在线观看视频日韩 | 亚洲精品婷婷 | 成人h在线观看 | 99热在线免费观看 | 日韩精品视频久久 | 国产精品自产拍在线观看桃花 | 久久免费视频网站 | 91av在线免费看 | 欧美a√在线 | 日韩欧美在线中文字幕 | 成人一区二区三区在线 | 激情综合色综合久久 | 色婷婷综合久色 | 亚洲国产精品日韩 | 亚洲欧美激情精品一区二区 | 中国一级特黄毛片大片久久 | 久久免费片| 在线国产不卡 | 激情久久一区二区三区 | 6699私人影院 | 黄色免费电影网站 | 欧美午夜寂寞影院 | 天天色宗合 | 国产精品99视频 | 91探花国产综合在线精品 | 久久久久久久99 | 成人看片 | 国产一级片观看 | 免费av电影网站 | 国产精品电影在线 | 亚洲成av人片在线观看 | 日韩理论在线视频 | 成人免费共享视频 | 黄av资源| 久久夜av | 中文在线a在线 | 99热精品在线观看 | 欧美一二三四在线 | 欧美日韩一区二区在线 | 亚洲视频在线免费看 | 丁香久久婷婷 | 亚洲粉嫩av| 久久免费视频国产 | 一区二区三区在线观看中文字幕 | 亚洲五月激情 | 成人国产一区二区 | 人人网av| 国产日韩精品在线 | 久久久精品影视 | 亚洲精品免费在线视频 | 国产精品女同一区二区三区久久夜 | 国产精品久久久久久久久蜜臀 | 国产精品嫩草影院9 | 国产又粗又猛又黄 | 国产青青青 | 亚洲第二色 | 在线之家免费在线观看电影 | 能在线观看的日韩av | 99视频久久 | 免费的黄色av | 日日夜夜狠狠操 | 最近最新mv字幕免费观看 | 色就是色综合 | 欧美久久九九 | 国产精品一区二区白浆 | 国产美女精品视频免费观看 | 91chinesexxx| 久操视频在线观看 | 超碰人人射 | 国产二区电影 | 欧美日韩高清国产 | 亚洲 中文 在线 精品 | 国产丝袜制服在线 | 久久午夜色播影院免费高清 | 中文字幕免费高清在线观看 | 美女免费视频网站 | 欧美成人精品三级在线观看播放 | 国内精品久久久久 | 国产伦精品一区二区三区无广告 | 很污的网站 | 日韩簧片在线观看 | 午夜婷婷综合 | 韩日色视频 | 日本性xxxxx| 91亚洲夫妻 | 色姑娘综合网 | www五月 | 国产日韩欧美在线观看视频 | 伊人影院得得 | 精品国产一区二区三区不卡 | 欧美精品一区二区免费 | 91视频最新网址 | 天天天综合 | 看黄色.com | 亚洲精品国产自产拍在线观看 | av黄色在线播放 | 97超碰站 | 97在线看 | 国产精品久久网站 | 久久男人中文字幕资源站 | 干天天| 国产精品99久久久久 | 99在线视频播放 | 精品国产aⅴ一区二区三区 在线直播av | 五月婷婷欧美视频 | 亚洲va综合va国产va中文 | 视频三区 | 日韩美av在线 | 国产破处在线视频 | 欧美尹人| av不卡网站 | 草久久影院 | 久久免费观看少妇a级毛片 久久久久成人免费 | 超碰在线91 | 丁香激情综合久久伊人久久 | 999久久久久久久久久久 | 中文字幕国产精品 | 91视频观看免费 | 国产精品1000| 天海翼一区二区三区免费 | 国内小视频 | 色哟哟国产精品 | 国产精品午夜久久久久久99热 | 欧美久草视频 | 少妇18xxxx性xxxx片 | 夜夜狠狠 | 久久丁香 | 国产黄在线| 久久精品99久久久久久 | 免费看一级一片 | 亚洲黄色app | 成人久久久久久久久久 | 亚洲精品综合一区二区 | 91精品久久香蕉国产线看观看 | 国产精品免费久久久久影院仙踪林 | 成人免费 在线播放 | 成人av电影网址 | 亚洲欧洲国产日韩精品 | 日本午夜在线观看 | 在线观看一区 | 丁香婷婷综合激情五月色 | 在线成人免费 | 亚洲1级片 | 国产一区二区高清不卡 | 久久人视频 | 中文字幕人成一区 | 9999亚洲| 亚洲精品在线国产 | 久久久久久久久久久免费 | 欧美一级免费片 | 91免费版在线 | 国产大片免费久久 | 久久综合一本 | 日本亚洲国产 | 99免在线观看免费视频高清 | 国产精品成人自产拍在线观看 | 永久免费看av| 久久激情视频 久久 | 99精品视频在线观看免费 | 国产中文字幕网 | 亚洲一区二区观看 | 91成人免费视频 | 日本不卡一区二区三区在线观看 | 久草视频中文在线 | 久久久www成人免费毛片麻豆 | 六月婷婷久香在线视频 | 一二三区视频在线 | 亚av在线| 午夜视频在线网站 | 蜜臀久久99精品久久久酒店新书 | 黄色大全免费观看 | 日韩免费视频线观看 | 久久国产精品色av免费看 | 国产精品成 | 激情综合站 | 不卡电影免费在线播放一区 | 国产亚洲亚洲 | 欧美激情精品久久久久 | 亚洲va欧洲va国产va不卡 | 国产一二三区av | 五月天激情开心 | 亚洲黄色激情小说 | 欧美精品国产综合久久 | 欧美在线视频免费 | 久久精品4| 伊人激情网 | 久久久久久久久国产 | 网站免费黄 | 中文字幕人成人 | 99re久久资源最新地址 | 91久久久国产精品 | 四虎欧美 | 五月综合网站 | 国产在线一线 | a v在线观看 | www五月婷婷 | 黄污网站在线观看 | 色婷婷在线观看视频 | 日韩一区二区三免费高清在线观看 | 亚洲97在线 | 日韩精品在线观看av | 国内精品久久久久久久97牛牛 | 1000部18岁以下禁看视频 | 国产99久久九九精品免费 | 婷婷色九月| 国产丝袜网站 | 九色琪琪久久综合网天天 | av一级网站 | 99视频精品在线 | 久久超碰97| 欧美在线a视频 | 狠狠狠色丁香婷婷综合激情 | 视频在线一区二区三区 | 免费在线色视频 | 国产精品久久久久久欧美 | 亚洲一区二区视频在线 | 丁香综合网 | 91麻豆福利 | 五月婷婷色播 | 成人黄色资源 | 日韩在线观看精品 | 国产 日韩 欧美 中文 在线播放 | 在线观看视频一区二区三区 | 激情网站| 精品久久久久_ | 日韩中文字幕一区 | 在线视频日韩一区 | 中文字幕高清视频 | 久久99国产一区二区三区 | 日本特黄特色aaa大片免费 | 99久久精品国产一区二区三区 | 成年人三级网站 | 综合五月婷婷 | 91爱爱中文字幕 | 国产精品18久久久久久不卡孕妇 | 亚洲综合干 | 超碰在线9 | 好看的国产精品视频 | 成人九九视频 | 成人av免费在线看 | 国产日韩中文字幕在线 | 日日草天天干 | 99视频精品全部免费 在线 | 国产系列 在线观看 | 免费毛片一区二区三区久久久 | 奇米7777狠狠狠琪琪视频 | 天天操夜夜操天天射 | 亚洲免费在线观看视频 | 久久精品日本啪啪涩涩 | 国产99一区| 日韩中文字幕国产精品 | 国产网站在线免费观看 | 色妞久久福利网 | 国产91对白在线 | av色网站| 99久久久久久久久 | 国产一区久久久 | 久久国产品 | 成人久久毛片 | 久久99亚洲网美利坚合众国 | 一本一本久久a久久精品牛牛影视 | 91成人网在线 | 免费看日韩 | 美女在线免费视频 | 婷婷婷国产在线视频 | 欧美中文字幕久久 | 一级成人网 | 日韩欧美一区视频 | 国产精品一区在线播放 | 天天操综合网站 | 亚洲欧美日韩在线看 | 亚洲午夜激情网 | 黄色毛片视频免费观看中文 | 91传媒在线看 | 天天操夜夜爱 | 天堂黄色片 | 免费高清在线一区 | 成人亚洲免费 | 午夜国产福利视频 | 婷婷免费在线视频 | 国产中文字幕视频 | 亚洲视频在线视频 | 成人免费亚洲 | 久草在线在线精品观看 | 午夜少妇av| 四虎永久免费网站 | 国产一区欧美日韩 | 久久婷婷激情 | 91人人澡人人爽 | 成年人电影免费看 | 高清免费av在线 | 亚洲一区天堂 | 成人理论电影 | 99精品在线免费观看 | 美女网站在线看 | 中文字幕中文中文字幕 | 欧美日本高清视频 | 久久久久久久久久影视 | 狠狠操欧美 | 激情欧美一区二区三区免费看 | 国产美女视频黄a视频免费 久久综合九色欧美综合狠狠 | 国产在线2020 | 高潮久久久久久 | 久久精品99久久久久久 | 夜夜操天天干, | 欧美日韩免费一区二区 | 欧美在线free | 日本性生活免费看 | 久久99国产综合精品免费 | 国产精品成人一区 | 久久人人添人人爽添人人88v | 色婷婷综合久久久久中文字幕1 | 国产精品久久中文字幕 | 国产色视频123区 | 99这里只有精品99 | 高清精品在线 | 麻豆久久一区二区 | 在线 影视 一区 | 麻豆一区二区 | 探花视频免费观看高清视频 | 999男人的天堂 | 四虎影视成人永久免费观看视频 | 欧美在线91 | 日韩一区二区三区免费视频 | 中文字幕在线一区二区三区 | 精品黄色视 | 国产精品久久久精品 | 亚洲精品国产拍在线 | 天天色视频 | 欧美日韩二三区 | 日韩欧美99| 五月婷社区 | 色wwww| 精品久久久久久久久久久久久久久久久久 | 日韩欧美在线视频一区二区 | 91久久影院 | 亚洲精品麻豆视频 | 中文字幕丝袜一区二区 | 夜夜操天天干, | 日韩影视大全 | 日韩一区二区免费在线观看 | 国产精品毛片久久久久久久久久99999999 | 久草在线久 | 色综合天天做天天爱 | 波多野结衣视频一区二区三区 | 一级黄色a视频 | 成人久久久精品国产乱码一区二区 | 国产小视频免费在线网址 | 在线免费观看黄色大片 | 国产在线不卡精品 | 中文字幕永久免费 | 国产一区二区精品久久 | 在线观看日本韩国电影 | 又黄又刺激的网站 | 视频在线在亚洲 | 免费在线观看av网址 | 三级av免费看| 国产日韩精品一区二区 | 亚洲欧美在线观看视频 | 国产成人精品一区二区三区 | 啪啪免费观看网站 | 天天操天天射天天插 | 91免费日韩 | 中文资源在线播放 | 国产成人精品久久久久 | 操操操综合 | 不卡的av电影在线观看 | 中字幕视频在线永久在线观看免费 | 久久国产视屏 | 天天操操操操操操 | 黄色一级片视频 | 精品黄色在线观看 | 午夜婷婷在线观看 | 亚洲最大成人免费网站 | 麻豆视频一区二区 | 视频在线观看91 | 婷婷成人亚洲综合国产xv88 | 国产在线欧美在线 | 成片视频在线观看 | 伊人小视频 | 日韩欧美极品 | 亚洲一区久久 | 91激情小视频 | 高清免费在线视频 | 久久精品欧美一 | 91香蕉视频在线下载 | 免费人人干 | 日韩中文字幕国产 | 伊香蕉大综综综合久久啪 | 午夜精品视频福利 | 国产精品美女久久 | 久久久久久久久久久久电影 | 在线观看黄色大片 | 天天干天天看 | 成人丁香花 | 成人禁用看黄a在线 | 日本一区二区不卡高清 | 日韩欧美在线视频一区二区 | 精品国产一区二区三区男人吃奶 | 视频在线99 | 欧美午夜性生活 | 亚洲成年片 | 特黄免费av | 在线观看日韩国产 | 国产精品久久久久一区二区三区共 | 人人爽人人香蕉 | 黄影院| 九九热久久免费视频 | 最新91在线视频 | 国产主播大尺度精品福利免费 | 成人国产精品久久久久久亚洲 | 中文字幕av影院 | 欧美精品久久99 | 黄色1级大片 | 婷婷伊人综合亚洲综合网 | 日日夜夜天天干 | 超碰97国产精品人人cao | 国产精品久久久99 | 国产精品久久久久一区二区国产 | 在线导航av | 国产直播av | 久久情侣偷拍 | 久草剧场 | 亚洲精品国产日韩 | 久久久久久久久久久网站 | 精品国产电影一区二区 | 欧美视频一区二 | 国产小视频在线 | 天无日天天操天天干 | 亚洲精品乱码久久久久久 | 在线观看岛国av | 免费一级片在线观看 | 国产精品久久久久久久久久久久 | 亚洲天堂视频在线 | 天天干天天干天天射 | 国模精品一区二区三区 | 香蕉国产91 | 久久精品一区二区三区国产主播 | 九九影视理伦片 | 午夜精品一区二区国产 | 夜夜视频欧洲 | 欧美视频在线观看免费网址 | 国产99精品| 国内精品毛片 | 91精品视屏 | 亚洲成人二区 | 久久精品综合网 | 久久综合欧美 | 一区二区视频电影在线观看 | 在线亚洲精品 | 91精品专区| 欧美一级日韩免费不卡 | 麻豆av一区二区三区在线观看 | 国产成人在线综合 | 国产高清在线a视频大全 | 欧美大片aaa | 在线免费观看国产黄色 | 蜜臀av一区二区 | 91精品毛片 | 久久99视频免费观看 | 久久久久婷| 99久久久免费视频 | 99久高清在线观看视频99精品热在线观看视频 | 国产视频一区在线 | www色 | 五月天色网站 | 天天色天天操综合网 | 在线观看日韩一区 | 一区二区三区手机在线观看 | 一区二区视频电影在线观看 | 久色婷婷 | 中文字幕在线观看资源 | 香蕉一区 | 国产亚洲精品久久 | 99久久婷婷国产精品综合 | 三级av在线免费观看 | 亚洲第一区在线观看 | 欧美电影在线观看 | 久久久久久久久爱 | 国产一区二区三区高清播放 | 亚洲自拍偷拍色图 | 在线天堂亚洲 | a久久久久久 | 国内精品久久久久久久久久久久 | 精品99久久久久久 | 国产不卡在线观看视频 | 欧美日韩大片在线观看 | 国内精品视频在线播放 | 亚洲精品玖玖玖av在线看 | 亚洲国产字幕 | 91九色最新 | 亚洲女同ⅹxx女同tv | 在线观看视频国产 |