0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebSocket冗長化

Last updated at Posted at 2024-05-11

フリーのWebSocketサービスは、意図せずサーバーが落ちていたり、いつのまにか仕様が変わっていたり、ぱっと使いたいときに動かないケースが多々あります。
気楽にWebSocketが使えるように、また有料サーバーの通信を強化する意味で、複数のソケット接続を束ねてメッセージ送受信できるものを作ってみた。

使用サーバー

サービス タイプ 強さ
Achex  無料 たまに落ちている
Scaledrone  無料 そこそこ安定
PieSocket 有料 安定 ※絶対落ちないわけではない

使い方

使用するパスでメッセージのチャンネルを切り替えています。
同じディレクトリにいるページ同士でメッセージ送受信

var outer = new Outer(
	{
		test	:e=>{console.log(e)},//タイプ指定受信
		dummy	:e=>{console.log(e)},
		sample	:e=>{console.log(e)},
    },
    e=>{console.log(e)}//全受信
)
button.onclick = e=>{
	outer.on({type:'test',value:'メッセージなど'})//送信
}

コード

test.html
<!DOCTYPE html>
<meta charset="UTF-8">
<script type="module">

//メッセージ送受信のテストは、このページを複数ブラウザで開く

import {Outer}	from './Outer.js'

class View{
	constructor(s,...a){
		this.node = document.createElement(s)
		this.add(...a)
	}
	add(...a){
		for(var v of a){
  			switch(typeof v){
				case 'function'	:this.assign({onclick:v})	;break
				default			:this.append(v)
			}
  		}
		return this
	}
	assign(...a){Object.assign(this.node,...a);return this}
	append(...a){a.forEach(v=>this.node.append(v.node||v));return this}
	flush(){document.body.append(this.node);return this}
}

var outer = new Outer(e=>{
	console.log(e)
	new View('div',JSON.stringify(e)).flush()
})

new View('button','send',e=>{
	var value = new Date().toLocaleString()
	outer.on({type:'test',value})
}).flush()

</script>
Outer.js
class Dispatcher{
	constructor(...a){
		this.hooks = new Listeners()
		this.add(...a)
	}
	add(...a){
		for(var v of a){
			switch(typeof v){
				case 'object'	:this.addObject(v)	;break
				case 'function'	:this.hooks.add(v)	;break
			}
		}
		return this
	}
	addObject(o){this.hooks.add(o)}
	on(...a){this.hooks.on(...a);return this}
}

export class Outer extends Dispatcher{
	constructor(...a){
		super()
		this.items	= []
		this.tags	= []
		this.add(
			new PieSocket(),
			new Scaledrone(),
			new Achex(),
			new Inner(),
			{RECONNECT:e=>{//再接続コマンド
				var offline = this.items.filter(v=>v.connecting!=true)
				offline.forEach(v=>v.open())
			}},
			...a
		).open()
		console.log(this)
	}
	addObject(o){
		if(o instanceof Socket){
			o.add({message:e=>this.onComming(e.value)})
			this.items.push(o)
		}else{
			super.addObject(o)
		}
	}

	open(){
 		//ルーム入場キーを作るのに叩いてる、phpでなくともよい
		var url		= import.meta.url.match(/.+\/\w+/)[0]+'.php'
		var body	= this.getLocation()
		fetch(url,{method:'POST',body}).then(r=>r.json()).then(data=>{
			console.log(data)
			this.items.forEach(v=>v.config(data).open())
		})
		return this
	}

	on(...a){return this.onGoing(...a)}
	
	onGoing(...a){//メッセージ送信
		var e = Object.assign({},...a,{tag:this.random()})
		this.record(e)
		this.items.forEach(v=>v.onGoing(e))
		return this
	}
	
	onComming(e){//メッセージ受信
		if(this.includes(e)) return this
		this.record(e)
		this.hooks.on(e)
		return this
	}

	includes(e){
		var entry = this.tags.find(v=>v[0]==e.tag)
		if(entry){
			var ms = performance.now()-entry[1]
			console.log(e.tag,'+'+ms.toFixed(1)+'ms',e.via)//エコー
			return true
		}else{
			return false
		}
	}
	record(e){
		this.tags.push([e.tag,performance.now()])
		this.tags = this.tags.slice(-30)
	}
 	getLocation(){
		var s = location.href.split('?')[0]
		var a = s.split('/').slice(0,-1)
		return a.join('/')
	}
	random(){return Math.random().toString(36).slice(2)}
}


class Socket extends Dispatcher{
	get connecting(){return this.client?.readyState==WebSocket.OPEN}
	get name(){return this.constructor.name}
	config(hash){return this}
	open(){return this}
	close(){
		if(this.connecting) this.client.close()
		return this
	}
	onComming(data){
		var e = JSON.parse(data)
		e.via = this.name//経路、デバッグ用
		return this.on({type:'message',value:e})
	}
	onGoing(e){
		if(this.connecting) this.send(e)
		return this
	}
	send(...a){
		var event = Object.assign({},...a)
		return this.client.send(JSON.stringify(event))
	}
}

class Inner extends Socket{
	//ローカルストレージのダミーソケット
	constructor(){
		super()
		this.key 		= null
		this.client		= this
		this.readyState = WebSocket.CLOSED
		window.addEventListener('storage',e=>{
			if(this.connecting && e.newValue && e.key==this.key){
				this.onComming(e.newValue)
			}
		})
	}
	config(hash){
		this.key = [hash.SHA256S,this.name].join("\n")
		return this
	}
	open(){ 
		this.readyState = WebSocket.OPEN	
		return this.on({type:'status',value:'open'})
	}
	close()	{ 
		this.readyState = WebSocket.CLOSED
		return this.on({type:'status',value:'close'})
	}
	send(e){
		localStorage.removeItem(this.key)
		localStorage.setItem(this.key,JSON.stringify(e))
		return this
	}
}

class PieSocket extends Socket{
	//https://piehost.com/
	config(hash){
		var API_KEY		= 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'//管理画面から
		this.request	= {url:`wss://free3.piesocket.com/v3/${hash.MD5S}?api_key=${API_KEY}`}
		return this
	}
}

class Scaledrone extends Socket{
	//https://www.scaledrone.com/
	config(hash){
		var channel		= 'XXXXXXXXXXXXXXXX'//管理画面から
		var room		= hash.SHA256S
		this.request	= {
			url			: 'wss://api.scaledrone.com/v3/websocket',
			handshake	: {type:'handshake',channel,version:2,callback:1},
			subscribe	: {type:'subscribe',room,callback:2},
			publish		: {type:'publish',room,message:{}},
		}
		return this
	}
	open(){
		if(this.connecting) return this
		if(this.request){
			this.client = Object.assign(new WebSocket(this.request.url),{
				onmessage	:message=>{
					var data = JSON.parse(message.data)
					this.on({type:'status',value:data})
					switch(data.callback){
						case 1	: this.subscribe(data)								;break
						case 2	: this.client.onmessage = e=>this.onComming(e.data)	;break
					}
				},
				onopen	:e=>this.on({type:'status',value:e}).send(this.request.handshake),
				onclose	:e=>this.on({type:'status',value:e}),
				onerror	:e=>this.on({type:'status',value:e}),
			})
		}
		return this
	}
	subscribe(e){
		if('error' in e) return this.close()//channel入れなくてもエラーは来ないので閉じる
		return this.send(this.request.subscribe)
	}
	onComming(data){
		var v = JSON.parse(data)
		if(v?.message?.type){
			var e = v.message
			e.via = this.name
			this.on({type:'message',value:e})
		}else{
			this.on({type:'status',value:v})
		}
		return this
	}
	onGoing(e){
		if(this.connecting) this.send(this.request.publish,{message:e})
		return this
	}
}

class Achex extends Socket{
	//https://www.achex.ca/
	config(hash){
		var hub	= hash.SHA256S
		this.request = {
			url		: 'wss://cloud.achex.ca/'+hub,
			auth	: {auth:this.random()},
			join	: {joinHub:hub},//passwd効いてない
			to		: {toH:hub},
		}
		return this
	}
	open(){
		if(this.connecting) return this
		if(this.request){
			this.client = Object.assign(new WebSocket(this.request.url),{
				onmessage	:message=>{
					var data = JSON.parse(message.data)
					this.on({type:'status',value:data})
					switch('OK'){
						case data.auth		: this.send(this.request.join)							;break
						case data.joinHub	: this.client.onmessage = e=>this.onComming(e.data)		;break
					}
				},
				onopen	:e=>this.on({type:'status',value:e}).send(this.request.auth),
				onclose	:e=>this.on({type:'status',value:e}),
				onerror	:e=>this.on({type:'status',value:e}),
			})
		}
		return this
	}
	onComming(data){
		var e = JSON.parse(data)
		if('type' in e){
			e.via = this.name
			this.on({type:'message',value:e})
		}else{
			this.on({type:'status',value:e})
		}
		return this
	}
	onGoing(e){
		if(this.connecting) this.send(e,this.request.to)
		return this
	}
	random(){return Math.random().toString(36).slice(2)}
}


class Listeners extends Array{
	add(...a){
		for(var v of a){
			switch(typeof v){
				case 'object'	:this.addObject(v)		;break
				case 'function'	:this.push([true,v])	;break
			}
		}
		return this
	}
	addObject(o){
		if(o==null) return
		switch(o.constructor){
			case Object	:this.addEntry(...Object.entries(o));break
			case Array	:this.addEntry(o)					;break
		}
	}
	addEntry(...entries){
		for(var entry of entries){
			if(typeof entry[1]=='function') this.push(entry)
		}
	}
	on(...a){
		var e = Object.assign({},...a)
		if('type' in e) this.dispatch(e)
		return this
	}
	dispatch(e){
		this.filter(a=>{
			if(a[0]==e.type) return true
			return a[0]==true
		}).forEach(a=>a[1](e))
		return this
	}
}
Outer.php
header('Content-Type: text/javascript');
echo json_encode(getKey(file_get_contents('php://input')));

function getKey($k){
	$data = array(
		'KEY'		=> $k,
		'SHA256'	=> hash('sha256',$k),
		'SHA256S'	=> '',
		'MD5'		=> hash('md5',$k),
		'MD5S'		=> '',
	);
	$data['SHA256S']	= short($data['SHA256']);
	$data['MD5S']		= short($data['MD5']);
	return $data;
}

function short($hash){
	$s = base64_encode(pack('H*',$hash));
	return strtr(rtrim($s,'='),'+/','-_');
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?