--- tags: ASP.NET --- # ASP.NET - SignalR 理解 **gitlab專案連結** : [[signalr-client]](http://192.168.1.136/robin98727/signalr-client)[[signalr-server]](http://192.168.1.136/Zap/signalr-server) **SignalR簡介** : [[SignalR簡介]](https://docs.microsoft.com/zh-tw/aspnet/signalr/overview/getting-started/introduction-to-signalr) ## 1. SignalR是什麼? ASP.NET SignalR 是 ASP.NET 開發人員適用的程式庫,可用來將任何類型的 「 即時 」 的 web 功能新增至您的 ASP.NET 應用程式 >即時 web 功能 : >能夠有伺服器程式碼推送內容至連線的用戶端立即可供使用,而不需要伺服器等候用戶端要求新的資料。 ## 2. 指定傳輸 如果已知的用戶端並不支援任何其他通訊協定所使用的連接,則會使用 Ajax 長輪詢傳輸 connection.start({ transport: 'longPolling' }); 如果您想要嘗試在順序中的特定傳輸的用戶端,您可以指定後援的順序 connection.start({ transport: ['webSockets','longPolling'] }); 指定傳輸的字串常數定義 : * webSockets * foreverFrame * serverSentEvents * longPolling ## 3. 名詞介紹 **1. 輪詢(polling)** ![Imgur](https://i.imgur.com/skMntOO.png) **2. 長輪詢(Long polling)** ![Imgur](https://i.imgur.com/ON2jjhS.png) **3. WebSocket** ![Imgur](https://i.imgur.com/Ao7EbOd.png) ## 4. Signalr-Client 程式講解(實作聊天室) ### 一、目錄架構 ``` Signalr-Client |_____ app |_____actions | |_____index.js |_____components | |_____App.jsx | |_____MsgForm.jsx | |_____MsgList.jsx | |_____SignUpForm.jsx |_____containers | |_____App.jsx | |_____MsgForm.jsx | |_____MsgList.jsx | |_____SignUpForm.jsx |_____lib | |_____api.js |_____reducers | |_____index.js | |_____msg.js | |_____user.js |_____sagas | |_____index.js | |_____msg.js | |_____user.js |_____signalr | |_____index.js ... ``` ### 二、進入登入畫面 **1. user連到畫面時,透過SignalR,與server建立Hub connection,產生出Hub proxy** ![Imgur](https://i.imgur.com/kP0zEmD.png) ==signalr/index.js== ```javascript= import { hubConnection } from 'signalr-no-jquery'; import { api } from "../../../config.json"; import * as actions from '../actions' /*api為http://localhost:50890*/ export default function signalr(dispatch) { //手動方式建立可定義的proxy const connection = hubConnection(api); const msgHubProxy = connection.createHubProxy('messaging'); //等待從web推送過來的訊息 msgHubProxy.on("receiveMsg", function (data) { dispatch(actions.receive_msg(data)) }) return connection } ``` **2. user輸入姓名以及房號時,分別觸發`CHANGE_NAME`、`CHANGE_ROOM`這兩個action** ![Imgur](https://i.imgur.com/fY7P8Po.png) ==actions/index.js== ```javascript= //更改名稱 export const CHANGE_NAME = 'CHANGE_NAME' export const chanege_name = (name) => action(CHANGE_NAME, { name }) //更改房名 export const CHANGE_ROOM = 'CHANGE_ROOM' export const chanege_room = (gid) => action(CHANGE_ROOM, { gid }) ``` ==reducers/user.js== ```javascript= import * as actions from '../actions' export default function ( state = { name: '', //使用者名稱 gid: '', //房間號碼 } , action) { switch (action.type) { case actions.CHANGE_NAME: let {name}=action return { ...state, name, } case actions.CHANGE_ROOM: let {gid}=action return { ...state, gid, } default: return state } } ``` ==containers/SignUpForm.jsx== ```javascript= import { connect } from "react-redux"; import { chanege_name,chanege_room,signup_user } from "../actions"; import SignUpFrom from '../components/SignUpForm'; const mapStateToProps =(state) =>{ const {user} = state return {user} //user有name、gid、connectionId、error、isfail } const mapDispatchToProps = (dispatch) =>{ return { chanege_name: name=>dispatch(chanege_name(name)), chanege_room: gid=>dispatch(chanege_room(gid)), } } export default connect( mapStateToProps, mapDispatchToProps )(SignUpFrom) ``` ==components/SignUpForm.jsx== ```javascript= import React, { Component } from 'react' export class SignUpForm extends Component { constructor(props) { super(props); this.handleNameChange = this.handleNameChange.bind(this); this.handleRoomChange = this.handleRoomChange.bind(this); } handleNameChange(event){ this.props.chanege_name(event.target.value); } handleRoomChange(event){ this.props.chanege_room(event.target.value); } render() { let {user} = this.props, {name,gid,error} = user return ( <form onSubmit={this.handleSubmit}> <label>姓名:<input value={name} onChange={this.handleNameChange} placeholder='請輸入姓名'/></label> <br/> <label>房號:<input value={gid} onChange={this.handleRoomChange} placeholder='請輸入房間ID'/></label> <br/> <button>登入</button> {error?<div>{error}</div>:null} </form> ) } } export default SignUpForm ``` **3. user按下登入鍵後觸發`SIGNUP_USER`、`SIGNUP_SUCCESS`、`SIGNUP_FAIL`這三個action** ![Imgur](https://i.imgur.com/Ollc8SG.png) ==actions/index.js== ```javascript= //發出使用者登入 export const SIGNUP_USER = 'SIGNUP_USER' export const signup_user = (data) => action(SIGNUP_USER, { data }) export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS' export const signup_success = (data) => action(SIGNUP_SUCCESS, data) export const SIGNUP_FAIL = 'SIGNUP_FAIL' export const signup_fail = (error) => action(SIGNUP_FAIL, { error }) ``` ==reducers/user.js== ```javascript= import * as actions from '../actions' export default function ( state = { connectionId: null, error: '', isfail: false, } , action) { switch (action.type) { case actions.SIGNUP_USER: return { ...state, connectionId: null, isfail: false, error: '', } case actions.SIGNUP_SUCCESS: let { connectionId } = action return { ...state, isfail: false, error: '', connectionId, } case actions.SIGNUP_FAIL: let { error } = action return { ...state, isfail: true, error, } default: return state } } ``` ==containers/SignUpForm.jsx== ```javascript= import { connect } from "react-redux"; import { chanege_name,chanege_room,signup_user } from "../actions"; import SignUpFrom from '../components/SignUpForm'; //回傳user的資料給components const mapStateToProps =(state) =>{ const {user} = state return {user} } const mapDispatchToProps = (dispatch) =>{ return { signup_user: ()=> dispatch(signup_user()), } } export default connect( mapStateToProps, mapDispatchToProps )(SignUpFrom) ``` ==components/SignUpForm.jsx== ```javascript= import React, { Component } from 'react' export class SignUpForm extends Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); } handleSubmit(event){ let {user} = this.props, {name,gid} = user //如果名稱跟房名都有,就成功登入 if(name&&gid) this.props.signup_user(); event.preventDefault(); } render() { let {user} = this.props, {name,gid,error} = user return ( <form onSubmit={this.handleSubmit}> <label>姓名:<input value={name} onChange={this.handleNameChange} placeholder='請輸入姓名'/></label> <br/> <label>房號:<input value={gid} onChange={this.handleRoomChange} placeholder='請輸入房間ID'/></label> <br/> <button>登入</button> {error?<div>{error}</div>:null} </form> ) } } export default SignUpForm ``` ==sagas/index.js== ```javascript= import { fork } from 'redux-saga/effects' import signalr from '../signalr' import * as user from './user.js' export default function* (dispatch) { //當用戶一進入網頁,及透過signalr建立伺服器端與用戶的連線 const connect = signalr(dispatch) //等待有無發出使用者登入 yield fork(user.signUp, connect) } ``` ==sagas/user.js== ```javascript= import { take, call, put, select } from 'redux-saga/effects' import * as actions from '../actions' //建立連線 function connection_start(connect,gid) { connect.qs = {gid} return connect.start({ withCredentials: true, transport: ['webSockets', 'longPolling'] }) } //user登入 export function* signUp(connect) { while (true) { yield take(actions.SIGNUP_USER) try{ const gid = yield select(state=>state.user.gid) yield call(connection_start,connect,gid) //如果有拿到id,則成功登入 if(connect.id) yield put(actions.signup_success({connectionId: connect.id})) //沒拿到id,則登入失敗 else yield put(actions.signup_fail({error: JSON.stringify(connect.lastError)})) } catch(e){ yield put(actions.signup_fail(e.message)) } } } ``` ### 三、聊天室 **1. user輸入文字時,觸發`CHANGE_MSG`這個action** ![Imgur](https://i.imgur.com/83ybpF2.png) ==actions/index.js== ```javascript= //改變聊天欄的文字 export const CHANGE_MSG = 'CHANGE_MSG' export const change_msg = (text) => action(CHANGE_MSG, { text }) ``` ==reducers/msg.js== ```javascript= import * as actions from '../actions' export default function ( state = { text: '', } , action) { switch (action.type) { case actions.CHANGE_MSG: let { text } = action return { ...state, text, } default: return state } } ``` ==containers/MsgForm.jsx== ```javascript= import { connect } from "react-redux"; import { change_msg } from "../actions"; import MsgForm from '../components/MsgForm'; //回傳msg給components const mapStateToProps =(state) =>{ const {msg,user} = state return {msg,user} } const mapDispatchToProps = (dispatch) =>{ return { change_msg: text=>dispatch(change_msg(text)), } } export default connect( mapStateToProps, mapDispatchToProps )(MsgForm) ``` ==components/MsgForm.jsx== ```javascript= import React, { Component } from 'react' export class MsgForm extends Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); } //紀錄文字框輸入訊息 handleChange(event){ this.props.change_msg(event.target.value); } render() { let {msg,user} = this.props, {text} = msg, {name,connectionId} = user return ( <form onSubmit={this.handleSubmit}> <hr/> <b>{name}: </b> <input value={text} onChange={this.handleChange} placeholder='write something...'/> <button>新增</button> <address>(connectionId: {connectionId})</address> </form> ) } } export default MsgForm ``` **2. user按下新增後觸發`SEND_MSG`、`RECEIVE_MSG`、`CHANGE_MSG`這三個action** ![Imgur](https://i.imgur.com/SKg7n2B.png) > **CHANGE_MSG這個action跟上面一樣便不再描述** ==actions/index.js== ```javascript= //送出訊息 export const SEND_MSG = 'SEND_MSG' export const send_msg = (data) => action(SEND_MSG, { data }) //收到訊息 export const RECEIVE_MSG = 'RECEIVE_MSG' export const receive_msg = (data) => action(RECEIVE_MSG, { data }) ``` ==reducers/msg.js== ```javascript= import * as actions from '../actions' export default function ( state = { data: [{ sender_id: 0, //初始會顯示system:welcome sender_name: 'system', content: 'welcome~' }] } , action) { switch (action.type) { case actions.RECEIVE_MSG: let { data } = action return { ...state, data: [...state.data, data], //data會去新增user輸入的文字 } default: return state } } ``` ==constainers/MsgForm.jsx== ```javascript= import { connect } from "react-redux"; import { change_msg,send_msg } from "../actions"; import MsgForm from '../components/MsgForm'; const mapStateToProps =(state) =>{ const {msg,user} = state return {msg,user} } const mapDispatchToProps = (dispatch) =>{ return { send_msg: data=>dispatch(send_msg(data)), } } export default connect( mapStateToProps, mapDispatchToProps )(MsgForm) ``` ==containers/MsgList.jsx== ```javascript= import { connect } from "react-redux"; import { change_msg,send_msg } from "../actions"; import MsgList from '../components/MsgList'; const mapStateToProps =(state) =>{ const {msg} = state return {msg} //回傳user輸入的文字 } export default connect( mapStateToProps )(MsgList) ``` ==sagas/index.js== ```javascript= import { fork } from 'redux-saga/effects' import signalr from '../signalr' import * as msg from './msg.js' export default function* (dispatch) { //當用戶一進入網頁,及透過signalr建立伺服器端與用戶的連線 const connect = signalr(dispatch) //等待有無發出輸入訊息的請求 yield fork(msg.sendMsg, connect) } ``` ==sagas/msg.js== ```javascript= import { take, call, put, select } from 'redux-saga/effects' import * as actions from '../actions' import api from '../lib/api' //寫入API function send_msg({data,name,connectionId,gid}) { return api({ cmd: 'api/msg/send', cors: true,method: 'post', data:{ content: data, sender_id: connectionId, sender_name: name, gid, } }) } export function* sendMsg() { while (true) { const { data } = yield take(actions.SEND_MSG) const {name,connectionId,gid} = yield select(state=>state.user) const res = yield call(send_msg, {data,name,connectionId,gid}) //如果有成功向API POST則把欄位框的文字清空 if(res.body){ yield put(actions.change_msg('')) } } } ``` ==signalr/index.js== ```javascript= import { hubConnection } from 'signalr-no-jquery'; import { api } from "../../../config.json"; import * as actions from '../actions' export default function signalr(dispatch) { //手動方式建立可定義的proxy const connection = hubConnection(api); const msgHubProxy = connection.createHubProxy('messaging'); //等待從web推送過來的訊息 msgHubProxy.on("receiveMsg", function (data) { dispatch(actions.receive_msg(data)) }) return connection } ``` ==components/MsgForm.jsx== ```javascript= import React, { Component } from 'react' export class MsgForm extends Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); } //送出使用者輸入的文字 handleSubmit(event){ let {msg}=this.props, {text} = msg this.props.send_msg(text); event.preventDefault(); } render() { let {msg,user} = this.props, {text} = msg, {name,connectionId} = user return ( <form onSubmit={this.handleSubmit}> <hr/> <b>{name}: </b> <input value={text} onChange={this.handleChange} placeholder='write something...'/> <button>新增</button> <address>(connectionId: {connectionId})</address> </form> ) } } export default MsgForm ``` ==components/MsgList== ```javascript= import React, { Component } from 'react' export class MsgList extends Component { render() { const {msg} =this.props, {data} = msg return ( //list會顯示寄送者的名稱以及內容 <ul className="list"> {data.map((m,i)=>(<li key={i}> <a href={'#'+m.sender_id}>{m.sender_name}</a>: <span>{m.content}</span> </li>))} //點選寄送者後網址會顯示傳出者的ID </ul> ) } } export default MsgList ``` **3. 按下登出後會觸發`SINGOUT_USER`這個action** ![Imgur](https://i.imgur.com/5oe0cP4.png) ==actions/index.js== ```javascript= //user登出 export const SIGNOUT_USER = 'SIGNOUT_USER' export const signout_user = (data) => action(SIGNOUT_USER, { data }) ``` ==reducers/user.js== ```javascript= import * as actions from '../actions' export default function ( //user登出後,清空狀態 state = { name: '', gid: '', connectionId: null, error: '', isfail: false, } , action) { switch (action.type) { case actions.SIGNOUT_USER: return { ...state, name: '', connectionId: null, isfail: false, error: '', } default: return state } } ``` ==containers/App.jsx== ```javascript= import { connect } from "react-redux"; import { signout_user} from '../actions' import App from '../components/App'; const mapStateToProps =(state) =>{ const {user} = state return {user} } //dispatch sign_out這個action const mapDispatchToProps = (dispatch) =>{ return { signout_user: ()=>dispatch(signout_user()), } } export default connect( mapStateToProps, mapDispatchToProps )(App) ``` ==sagas/index.js== ```javascript= import { fork } from 'redux-saga/effects' import signalr from '../signalr' import * as msg from './msg.js' import * as user from './user.js' export default function* (dispatch) { //當用戶一進入網頁,及透過signalr建立伺服器端與用戶的連線 const connect = signalr(dispatch) //等待有無發出使用者登出 yield fork(user.signOut, connect) } ``` ==sagas/user.js== ```javascript= import { take, call, put, select } from 'redux-saga/effects' import * as actions from '../actions' function connnection_close(connect){ return connect.stop() } //user登出 export function* signOut(connect) { while (true) { const { data } = yield take(actions.SIGNOUT_USER) // error handler!? yield call(connnection_close,connect) } } ``` ==components/App.jsx== ```javascript= import React, { Component } from 'react' import PropTypes from 'prop-types' import MsgList from '../containers/MsgList' import MsgForm from '../containers/MsgForm' import SignUpForm from '../containers/SignUpForm' class App extends Component { constructor(props) { super(props); } render() { let {user,signout_user} = this.props, {name,isfail,error,connectionId,gid} = user return ( <div className="App"> <h1>對話系統{connectionId?<span> (個室:{gid})</span>:null}</h1> {connectionId? <button onClick={e=>signout_user()}>登出</button>: null} <hr/> {connectionId?[<MsgList key='MsgLis't/>,<MsgForm key='MsgForm'/>,]:<SignUpForm/>} </div> ); } } export default App; ``` ## 幾個問題 ### Q1 聊天室登出後,畫面會留著房號 ### Q2 登出會觸發兩次SIGNOUT_USER的action ![Imgur](https://i.imgur.com/n0KK9MZ.png) 程式碼 ==sagas/user.js== ```javascript= function connnection_close(connect){ return connect.stop() } export function* signOut(connect) { while (true) { const { data } = yield take(actions.SIGNOUT_USER) // error handler!? yield call(connnection_close,connect) yield put(actions.signout_user()) } } ``` ==actions/index.js== ```javascript= export const SIGNOUT_USER = 'SIGNOUT_USER' export const signout_user = (data) => action(SIGNOUT_USER, { data }) ``` ==reducers/user.js== ```javascript= import * as actions from '../actions' export default function ( state = { name: '', gid: '', connectionId: null, error: '', isfail: false, } , action) { switch (action.type) { case actions.SIGNOUT_USER: return { ...state, name: '', connectionId: null, isfail: false, error: '', } default: return state } } ``` ### Q3 signalr裡面的index2原本是要做甚麼的? ==signalr/index2.js== ```javascript= import { hubConnection } from 'signalr-no-jquery'; import { api } from "../../../config.json"; import * as actions from '../actions' export default function signalr(store) { const connection = hubConnection(api); const msgHubProxy = connection.createHubProxy('messaging'); msgHubProxy.on("receiveMsg", function (data) { console.log("receiveMsg: ", data); store.dispatch(actions.receive_msg(data)) }) connection.start({ jsonp: true, transport: ['webSockets', 'longPolling'] }) .done(function () { console.log('Now connected, connection ID=' + connection.id); }) .fail(function () { console.log('Could not connect'); }); } ```