及時通信最佳實踐

###描述


Lhttp是一個基於websocket服務端框架,提供一個類似http的協議去幫助開發者開發長連接的應用。


使用Lhttp可以大量減少服務端開發的工作量,實現非常好的模塊化和業務功能的解耦合。


可以定製任何你想要的功能。


[項目地址](https://github.com/fanux/lhttp)


### 特點

*   使用簡單,功能強大

*   性能高,使用gnatsd消息隊列 publish 10000 條消息耗時0.04s(single-core CPU,1G memory).

*   支持集羣,橫向擴展,通過增加服務器來獲取更高的服務能力

*   非常容器進行定製與擴展

*   可以非常好的與http服務協同工作,如利用http發送消息,將消息轉發給上游http服務器等。所以即便你不會go語言也可以開發一些應用。


###  [聊天室demo](https://github.com/fanux/lhttp-web-demo)

![chat-demo](https://github.com/fanux/lhttp-web-demo/blob/master/web-demo.gif)

[前端sdk](https://github.com/fanux/lhttp-javascript-sdk)



####協議棧:

```go

+--------------------+

|       lhttp        |

+--------------------+

|     websocket      |

+--------------------+

|        TCP         |

+--------------------+

```


####系統架構

```go

        +---------------------------------------+

        |    message center cluster (gnatsd)    |

        +---------------------------------------+

 ........|.................|...............|..................

| +-------------+   +-------------+   +-------------+        | 

| |lhttp server |   |lhttp server |   |lhttp server |   ...  |  lhttp 服務集羣

| +-------------+   +-------------+   +-------------+        | 

 .....|..........._____|  |___.............|  |_________......

      |          |            |            |            |       <----使用websocket鏈接

 +--------+  +--------+   +--------+   +--------+   +--------+   

 | client |  | client |   | client |   | client |   | client |   

 +--------+  +--------+   +--------+   +--------+   +--------+  

```


####快速入門

```bash

go get github.com/nats-io/nats

go get github.com/fanux/lhttp

```

先啓動gnatsd:

```bash

cd bin

./gnatsd &

./lhttpd 

```


打開另一個終端,執行客戶端程序,輸入命令碼:

```bash

cd bin

./lhttpClient

```


###使用docker快速體驗

```

$ docker build -t lhttp:latest .

$ docker run -p 9090:9090 -p 8081:8081 lhttp:latest

```

打開瀏覽器,訪問: `http://localhost:9090`.


打開兩個窗口就可以聊起來了。


websocket 端口是 8081, 可以使用自己的websocket客戶端去連 `ws://localhost:8081`


也可以從dockerhub上下載鏡像:

```

$ docker run -p 9090:9090 -p 8081:8081 fanux/lhttp:latest

```


###協議介紹

```go

LHTTP/1.0 Command\r\n                --------起始行,協議名和版本,Command:非常重要,標識這條消息的命令碼是什麼,服務端也是根據命令碼註冊對應的處理器的。

Header1:value\r\n                    --------首部

Header2:value\r\n

\r\n

body                                 --------消息體

```


事例:

```go

LHTTP/1.0 chat\r\n                  命令碼叫`chat`

content-type:json\r\n               消息體使用json編碼

publish:channel_jack\r\n            服務端請把這條消息publish給jack (jack訂閱了channel_jack)

\r\n

{

    to:jack,

    from:mike,

    message:hello jack,

    time:1990-1210 5:30:48

}

```

###使用教程,只需三步

 > 定義你的處理器,需要聚合 ```BaseProcessor```

 

```go

type ChatProcessor struct {

    *lhttp.BaseProcessor

}

```


> 實現三個接口,連接打開時幹嘛,關閉時幹嘛,消息到來時幹嘛。

```go

type ChatProcessor struct {

}

func (p ChatProcessor)OnOpen(h *WsHandler) {

    //your logic

}

func (p ChatProcessor)OnClose(h *WsHandler) {

    //your logic

}

func (p ChatProcessor)OnMessage(h *WsHandler) {

    //your logic

}

```


> 註冊你的處理器,這裏的`chat` 與消息體中的`chat對應`,也就是這個處理器僅會處理`LHTTP/1.0 chat\r\n....`這類消息.

```go

lhttp.Regist("chat",&ChatProcessor{&lhttp.BaseProcessor{}})

```

如果命令碼是 "chat" ChatProcessor 會處理它。


> 這裏比如收到消息就直接將消息返回:

```go

func (p *ChatProcessor)OnMessage(h *WsHandler) {

    h.Send(h.GetBody())

}

```

### 啓動服務器

```go

http.Handler("/echo",lhttp.Handler(lhttp.StartServer))

http.ListenAndServe(":8081")

```


### 一個完整的回射例子:

```go

type ChatProcessor struct {

    *lhttp.BaseProcessor

}


func (p *ChatProcessor) OnMessage (h *lhttp.WsHandler) {

    log.Print("on message :", h.GetBody())

    h.Send(h.GetBody())

}


func main(){

    lhttp.Regist("chat", &ChatProcessor{&lhttp.BaseProcessor{}})


    http.Handle("/echo",lhttp.Handler(lhttp.StartServer))

    http.ListenAndServe(":8081",nil)

}

```

***


### 訂閱/發佈 

下面來看用Lhttp開發及時通信應用有多簡單


假設有兩個客戶端,這裏的客戶端比如瀏覽器應用。


client1:

```go

LHTTP/1.0 command\r\n

subscribe:channelID\r\n

\r\n

body optional

```

client1通過websocket向Lhttp發送如上字符串,就訂閱了`channelId`


client2:

```go

LHTTP/1.0 command\r\n

publish:channelID\r\n

\r\n

body require

```

client2通過websocket向Lhttp發送如上字符串,就向`channelID`發佈了一條消息。  因爲client1訂閱了channelID,所以client1會收到這條消息。



client1不想再收消息,那麼發如下字符串給服務端即可:

```go

LHTTP/1.0 command\r\n

unsubscribe:channelID\r\n

\r\n

body optional

```

訂閱/發佈 是lhttp內置功能,服務端一行代碼不用寫即可獲取這種服務,只需要使用特定首部`subscribe`,`publish` 和`unsubscribe`


同時訂閱多個,如同時訂閱多個聊天室。

```go

LHTTP/1.0 chat\r\n

subscribe:channelID1 channelID2 channelID3\r\n

\r\n

```

####使用http發佈消息

URL: /publish . 

方法: POST . 

http body: 整個lhttp消息

for example I want send a message to who subscribe channel_test by HTTP.

如我想發送一條消息給訂閱了channel_test的人。

```go

    resp,err := http.POST("https://www.yourserver.com/publish", "text/plain",

    "LHTTP/1.0 chat\r\npublish:channel_test\r\n\r\nhello channel_test guys!")

```


這裏封裝好了一個更好用的工具 ```Publish```  tools.go

```go

//func Publish(channelID []string, command string, header map[string]string, body string) (err error) {

//}

//send message to who subscribe mike.


Publish("mike", "yourCommand", nil, "hello mike!")

```


###上游服務器

upstream首部可以讓lhttp向上遊的http服務器發送一條消息。

```go

LHTTP/1.0 command\r\n

upstream:POST http://www.xxx.com\r\n

\r\n

body

```

如果是POST方法,lhttp會把整個消息體當作http的body發送給 http://www.xxx.com

如果是GET,lhttp會忽略消息體


```go

LHTTP/1.0 command\r\n

upstream:GET http://www.xxx.com?user=user_a&age=26\r\n

\r\n

body

```


#### upstream有什麼用:

如我們不想改動lhttp的代碼,但是想存儲聊天記錄。


通過upstream可以實現很好的解耦:


並且http server可以用其它語言實現.


```

        +----+                  +----+

        |jack|                  |mike|

        +----+                  +----+

         |_____________    _______|

                       |  |

                   +------------+

                   |lhttp server|

                   +------------+

                         |(http request with chat record)

                         V

                   +------------+

                   | http server|  upstream server(http://www.xxx.com/record)

                   +------------+

                   (save chat record)

    

```

jack:

```go

LHTTP/1.0 chat\r\n

upstream:POST http://www.xxx.com/record\r\n

publish:channel_mike\r\n

\r\n

hello mike,I am jack

```

mike:

```go

LHTTP/1.0 chat\r\n

subscribe:channel_mike\r\n

\r\n

```

這樣jack publish消息時不僅mike可以收到,後端的upstream server也可以收到,我們可以在後端服務器中處理消息存儲的邏輯,如將消息


存儲到redis的有序集合中。



### 分塊消息

試想一下,一條消息中既有圖片也有文字還有語音怎麼辦? lhttp的multipart首部解決這個問題


```go

LHTTP/1.0 upload\r\n

multipart:0 56\r\n

\r\n

content-type:text/json\r\n

\r\n

{filename:file.txt,fileLen:5}

content-type:text/plain\r\n

\r\n

hello

```

```go

content-type:text/json\r\n\r\n{filename:file.txt,fileLen:5}content-type:text/plain\r\n\r\nhello

^                                                          ^

|<---------------------first part------------------------->|<---------second part------------>|

0                                                          56                           

```

http中是使用boundry實現的,lhttp使用偏移量標識分塊,這樣效率更高,不需要遍歷整個消息體。


#### 如何獲取分塊消息

如客戶端消息如下:

```go

LHTTP/1.0 upload\r\nmultipart:0 14\r\n\r\nk1:v1\r\n\r\nbody1k2:v2\r\n\r\nbody2

```

服務端代碼,消息存在鏈表中:

```go

type UploadProcessor struct {

*lhttp.BaseProcessor

}


func (*UploadProcessor) OnMessage(ws *lhttp.WsHandler) {

for m := ws.GetMultipart(); m != nil; m = m.GetNext() {

log.Print("multibody:", m.GetBody(), " headers:", m.GetHeaders())

}

}


//don't forget to tegist your command processor


lhttp.Regist("upload", &UploadProcessor{&lhttp.BaseProcessor{}})

```

###[首部過濾模塊開發](https://github.com/fanux/lhttp/blob/master/doc/DEVELOP.md)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章