hangscer

基于websocket与scalajs群聊功能demo

2017/05/05

什么是websocket?

http协议本身不支持持久连接(长连接 轮训不算).http协议有1.0和1.1两种,差别是所谓的keep-alive.

  1. http的生命周期通过request来界定,也就是一个request对应一个response,在http1.0中,这个http请求也就结束了🔚.
  2. 在http1.1中就行了改进,在keep-alive作用下,一个request,接受多个response.
  3. 总之,response是被动发送的,而不是主动发起的.

websocket借用了http的协议来完成一部分握手,但是websocket不是http协议.

下面是一个典型的websocket握手.
客户端发起:

1
2
3
4
5
6
7
8
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

这段类似http协议的握手请求中多个几个项目:Upgrade:websocketConnection:Upgrade,目的在于告诉服务器,客户端发起的是websocket而不是http协议.
Sec-WebSocket-Key:x3JJHMbDL1EzLkh9GBhXDw==,这个字段是浏览器生成的Base64随机码,目的在于验证服务器是不是真正的WebSocket协议.

服务器端回复:

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

这里开始就是http最后负责的区域了,告知客户端,服务器端已经切换协议了:Upgrade:websocketConnection:Updrade.
然后Sec-WebSocket-Accept字段是经过服务器确认,并且经过加密后的Sec-WebSocket-Key.

demo

实现这个demo,熟悉了scalajs,又熟悉了playframework.思路是在server端维护着一个map,其中是用户名与其ActorRef相对应,如var onLineMap:Map[String,ActorRef]=Map(),当有新连接进入时,如def preStart()方法,

1
2
3
4
5
6
7
8
override def preStart(): Unit = {
Storge.onLineMap.par.foreach{
case (_,actor)=>actor!username+",JOIN IN"
}//向已经在线的每个端发送消息
Storge.onLineMap=Storge.onLineMap+(username->self)
println(username+" :come in")
println(Storge.onLineMap)
}

server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class WebSocketCustomerController @Inject()( implicit system:ActorSystem ,materializer:Materializer) {
def socket(username:Option[String])=WebSocket.accept[String,String]{request=>
ActorFlow.actorRef(out=> MyWebSocketActor.props(username.get,out))
}
}
object MyWebSocketActor{
def props(username:String,out:ActorRef)=Props(classOf[MyWebSocketActor],username,out)
}
case class MyWebSocketActor(username:String="",out:ActorRef) extends Actor{
override def preStart(): Unit = {
Storge.onLineMap.par.foreach{
case (_,actor)=>actor!username+",JOIN IN"
}
Storge.onLineMap=Storge.onLineMap+(username->self)
println(username+" :come in")
println(Storge.onLineMap)
}
override def postStop(): Unit = {
Storge.onLineMap=Storge.onLineMap-username
println(username+" :level out")
println(Storge.onLineMap)
Storge.onLineMap.par.foreach{
case (_,actor)=>actor!username+" :lEAVE OUT"
}
}
override def receive: Receive = {
case msg:String if(msg.startsWith("all"))=>
Storge.onLineMap.par.foreach{
case (_,actor)=>actor!msg.split("-")(1)
}
case msg:String =>
out!msg
}
}
object Storge{
var onLineMap:Map[String,ActorRef]=Map()
}

client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
object TutorialApp extends JSApp {
var webSocketOpt:Option[WebSocket]=None
@JSExport
def connectToServer()={
getwebSocket
}
@JSExport
def sendMessage()={
val msg=webapp.utils.getEleById[HTMLInputElement]("msgtoSend").value
webSocketOpt match {
case Some(webSocket) =>webSocket.send("all-"+getUsername+" : "+msg)
case None =>()
}
}
def getwebSocket={
webSocketOpt=Some(new WebSocket("ws://localhost:9000/ws?username="+getUsername))
webSocketOpt match {
case Some(webSocket)=>
webSocket.onopen=webapp.utils.onOpen
webSocket.onmessage=webapp.utils.onMessage
case None=> println("getwebSocket:websocket none")
}
}
}
object utils{
def getUsername=getEleById[HTMLInputElement]("username").value
def getEleById[T](id:String):T=document.getElementById(id).asInstanceOf[T]
def onMessage=(msgEvent:MessageEvent)=>msgEvent.data match {
case msg:String =>
val p=document.createElement("p")
p.appendChild(document.createTextNode(msg))
getEleById[HTMLDivElement]("main_div_1").appendChild(p)
}
def onOpen=(event:Event)=>{
println("getwebSocket:onopen")
}
}

项目地址