数据通信的方式有很多种,其中 websocket 就是一种用于 IM 的常用数据通信方式,如在线客服、QQ、微信等,或多或少都使用到了这一技术。所以了解以及掌握 websocket 是很有必要的
废话不多说,先上图:
认识 WebSocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
在实现过程冲,由于原生的 WebSocket 存在兼容问题,所以一般在真正项目开发中,往往会用到一些三方常用的库,如 socket.io
、socket.io-client
等。
服务器端
简单地通过 nodejs+koa+socket.io 去进行模拟实现,其代码如下:
server.js
const http=require('http');
const Koa=require('koa');
const io=require('socket.io');
const uuid=require('uuid/v4');
//
let server=new Koa();
//
let httpServer=http.createServer(server.callback());
httpServer.listen(8080);
//
let wsServer=io.listen(httpServer);
wsServer.on('connection', sock=>{console.log('connected');
const ID=uuid();
sock.emit('ID', ID);
sock.on('msg', (user, msg)=>{wsServer.emit('broadcast', ID, user, msg);
});
});
客户端
客户端主要有三部分组成:登录、发送消息、展示消息,其中登录对于的组件是Login.js
, 消息相关的组件是Msg.js
。看下项目整体目录机构:
.
+-- public
+-- node_modules
+-- _src
| +-- _assets
| +-- _components
| +-- Login.js
| +-- Msg.js
| +-- _store
| +-- index.js
| +-- user.js
| +-- msg.js
| +-- actions.js
| +-- App.js
| +-- index.js
| +-- socket.js
+-- package.json
/src/socket.js: 单例实现全应用只有一个其实例对象,代码如下:
import io from 'socket.io-client';
const HOST="ws://localhost:8080/";
export default io(HOST);
src/store/index.js: 统一管理不同的 reducer,代码如下:
import {createStore,combineReducers} from 'redux'
import user from './user'
import msg from './msg'
export default createStore(combineReducers({
user,
msg
}))
src/store/user.js: 负责管理用户状态 (设置用户 ID、昵称等) 的 reducer,代码如下:
import {SET_USER_ID,SET_USER_NAME} from '../actions'
export default function(state = {ID:null,name:null},action){switch(action.type){
case SET_USER_ID:
return {
...state,
ID:action.value
}
case SET_USER_NAME:
return{
...state,
name:action.value
}
default:
return state
}
}
src/store/msg.js: 负责处理初始化消息、添加消息等状态的 reducer,代码如下:
import {ADD_MSG} from '../actions'
export default function(state = [],action){switch(action.type){
case ADD_MSG:
return [
...state,
action.msg
]
default:
return state
}
}
/src/actions.js: 统一管理整个应用的所有 action,代码如下:// 用户相关
export const SET_USER_ID = "set_user_id";
export const SET_USER_NAME = "set_user_name";
// 消息相关
export const ADD_MSG = "add_msg";
//-------------------------------
export function setUserId(ID){
return{
type:SET_USER_ID,
value:ID
};
}
export function setUserName(name){
return{
type:SET_USER_NAME,
value:name
};
}
export function addMsg(msg){
return{
type:ADD_MSG,
msg
}
}
src/index.js: 整个程序的入口,代码如下:
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter as Router,Route} from 'react-router-dom'
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import {async} from 'q';
import {Provider} from 'react-redux'
import store from './store'
import 'bootstrap/dist/css/bootstrap.css'
ReactDOM.render(
(<Provider store={store}>
<Router>
<Route component={App}/>
</Router>
</Provider>
),
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
src/App.js: 应用的主组件,负责路由的配置,数据的初始化,代码如下:
import React, {Component} from 'react';
import {connect} from 'react-redux'
import {Route,Redirect} from 'react-router-dom'
import Login from './components/Login'
import Msg from './components/Msg'
import {setUserId,addMsg} from './actions'
import socket from './socket'
class App extends Component {constructor(){super()
}
componentDidMount(){
// 得到初始化用户 ID
socket.on('ID',(ID) => {this.props.setUserId(ID);
})
// 接收消息
socket.on('broadcast',(from, fromUser, msg) => {this.props.addMsg({from, fromUser, msg})
})
}
render() {
return (
<div>
{
this.props.user.name ? '':(<Redirect to="/"/>)
}
<Route path="/" exact component={Login}/>
<Route path="/msg" component={Msg}/>
</div>
);
}
}
export default connect((state,props) => Object.assign({},props,state),{
setUserId,
addMsg
})(App);
src/components/Login.js: 登录组件,代码如下:
import React, {Component} from 'react';
import {connect} from 'react-redux'
import {setUserName} from '../actions'
class Login extends Component {constructor(){super();
this.rnd = Math.floor(Math.random()*1000000);
}
login = () => {
let username = this.refs.username.value;
this.props.setUserName(username);
this.refs.username.value = "";
this.props.history.push('/msg');
}
render() {
return (
<div className="panel panel-primary">
<div className="panel-heading">
<h2 className="panel-title">
登录
</h2>
</div>
<div className="panel-body">
<div className="from-group">
<label htmlFor={'username'+this.rnd}></label>
<input className="from-control" type="text" id={'username'+this.rnd} ref="username" placeholder="请输入用户名"/>
</div>
<div className="from-group" style={{'marginTop':'10px'}}>
<button type="button" className="btn btn-default" onClick={this.login}> 登录 </button>
</div>
</div>
</div>
);
}
}
export default connect((state,props) => Object.assign({},props,state),{setUserName})(Login);
src/components/Msg.js:发送消息、展示消息的组件,代码如下:
import React, {Component} from 'react';
import {connect} from 'react-redux'
import msg from '../store/msg';
import socket from '../socket'
class Msg extends Component {send = () => {console.log('this.props.user.username:',this.props.user.name)
socket.emit('msg',this.props.user.name,this.refs.msg.value);
this.refs.msg.value = ""
}
render() {
return (
<div>
<div>
<div className="from-group">
<textarea className="from-control" ref="msg"></textarea>
</div>
<div className="from-group">
<button className="btn btn-default" type="button" onClick={this.send}> 发送 </button>
</div>
</div>
<ul>
{this.props.msg.map(({from,fromUser,msg},index) => (<li key={index}>
<h3 className="list-group-item-heading" style={{color:from==this.props.user.ID ? 'red':''}}>{fromUser}</h3>
<p className="list-group-item-text">{msg}</p>
</li>
))
}
</ul>
</div>
);
}
}
export default connect((state,props) => Object.assign({},props,state),{})(Msg);