---
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)**

**2. 長輪詢(Long polling)**

**3. WebSocket**

## 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**

==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**

==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**

==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**

==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**

> **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**

==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

程式碼
==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');
});
}
```