Socket搭建即时通信服务器

webSecket

即时通信

  • 相关代码Demo地址, 内附服务端代码和iOS端聊天室测试Demo
  • 原文地址: Socket搭建即时通信服务器
  • 即时通信(Instant messaging,简称IM)是一个终端服务,容许两人或多人使用网路即时的传递文字讯息、档案、语音与视频交流
  • 即时通信按使用用途分为企业即时通信和网站即时通信
  • 根据装载的对象又可分为手机即时通信和PC即时通信,手机即时通信表明是短信,网站、视频即时通信

IM通讯原理

  • 客户端A与客户端B如何产生通讯?客户端A不能直接和客户端B,由于二者相距太远。
  • 这时就须要经过IM服务器,让二者产生通讯.
  • 客户端A经过socket与IM服务器产生链接,客户端B也经过socket与IM服务器产生链接
  • A先把信息发送给IM应用服务器,而且指定发送给B,服务器根据A信息中描述的接收者将它转发给B,一样B到A也是这样。
  • 通信问题: 服务器是不能主动链接客户端的,只能客户端主动链接服务器

即时通信链接原理

  • 即时通信都是长链接,基本上都是HTTP1.1协议,设置Connectionkeep-alive便可实现长链接,而HTTP1.1默认是长链接,也就是默认Connection的值就是keep-alive
  • HTTP分为长链接和短链接,其实本质上是TCP链接,HTTP协议是应用层的协议,而TCP才是真正的传输层协议, IP是网络层协议,只有负责传输的这一层才须要创建链接
  • 例如: 急送一个快递,HTTP协议指的那个快递单,你寄件的时候填的单子就像是发了一个HTTP请求。而TCP协议就是中间运货的运输工具,它是负责运输的,而运输工具所行驶的路就是所谓的TCP链接
  • HTTP短链接(非持久链接)是指,客户端和服务端进行一次HTTP请求/响应以后,就关闭链接。因此,下一次的HTTP请求/响应操做就须要从新创建链接。
  • HTTP长链接(持久链接)是指,客户端和服务端创建一次链接以后,能够在这条链接上进行屡次请求/响应操做。持久链接能够设置过时时间,也能够不设置

即时通信数据传递方式

目前实现即时通信的有四种方式(短轮询、长轮询、SSE、Websocketphp

短轮询:

  • 每隔一小段时间就发送一个请求到服务器,服务器返回最新数据,而后客户端根据得到的数据来更新界面,这样就间接实现了即时通讯
  • 优势是简单,缺点是对服务器压力较大,浪费带宽流量(一般状况下数据都是没有发生改变的)。
  • 主要是客户端人员写代码,服务器人员比较简单,适于小型应用

长轮询:

  • 客户端发送一个请求到服务器,服务器查看客户端请求的数据(服务器中数据)是否发生了变化(是否有最新数据),若是发生变化则当即响应返回,不然保持这个链接并按期检查最新数据,直到发生了数据更新或链接超时
  • 同时客户端链接一旦断开,则再次发出请求,这样在相同时间内大大减小了客户端请求服务器的次数.
  • 弊端:服务器长时间链接会消耗资源,返回数据顺序无保证,难于管理维护
  • 底层实现:在服务器的程序中加入一个死循环,在循环中监测数据的变更。当发现新数据时,当即将其输出给浏览器并断开链接,浏览器在收到数据后,再次发起请求以进入下一个周期

SSE

  • Server-sent Events服务器推送事件):为了解决浏览器只可以单向传输数据到服务端,HTML5提供了一种新的技术叫作服务器推送事件SSE
  • SSE技术提供的是从服务器单向推送数据给浏览器的功能,加上配合浏览器主动HTTP请求,二者结合起来,实际上就实现了客户端和服务器的双向通讯.

WebSocket

  • 以上提到的这些解决方案中,都是利用浏览器单向请求服务器或者服务器单向推送数据到浏览器
  • 而在HTML5中,为了增强web的功能,提供了websocket技术,它不只是一种web通讯方式,也是一种应用层协议
  • 它提供了浏览器和服务器之间原生的全双工跨域通讯,经过浏览器和服务器之间创建websocket链接,在同一时刻可以实现客户端到服务器和服务器到客户端的数据发送

WebSocket

  • WebSocket 是一种网络通讯协议。RFC6455 定义了它的通讯标准
  • WebSocket是一种双向通讯协议,在创建链接后,WebSocket 服务器和客户端都能主动的向对方发送或接收数据
  • WebSocket是基于HTTP协议的,或者说借用了HTTP协议来完成一部分握手(链接),在握手(链接)阶段与HTTP是相同的,只不过HTTP不能服务器给客户端推送,而WebSocket能够

WebSocket如何工做

  • Web浏览器和服务器都必须实现WebSockets协议来创建和维护链接。
  • 因为WebSockets链接长期存在,与典型的HTTP链接不一样,对服务器有重要的影响
  • 基于多线程或多进程的服务器没法适用于 WebSockets,由于它旨在打开链接,尽量快地处理请求,而后关闭链接
  • 任何实际的WebSockets服务器端实现都须要一个异步服务器

webServer

Websocket协议

协议头: ws, 服务器根据协议头判断是Http仍是websockethtml

// 请求头
     GET ws://localhost:12345/websocket/test.html HTTP/1.1
     Origin: http://localhost
     Connection: Upgrade
     Host: localhost:12345
     Sec-WebSocket-Key: JspZdPxs9MrWCt3j6h7KdQ==  
     Upgrade: websocket 
     Sec-WebSocket-Version: 13
    // Sec-WebSocket-Key: 叫“梦幻字符串”是个密钥,只有有这个密钥 服务器才能经过解码认出来,这是个WB的请求,要创建TCP链接了!!!若是这个字符串没有按照加密规则加密,那服务端就认不出来,就会认为这整个协议就是个HTTP请求。更不会开TCP。其余的字段均可以随便设置,可是这个字段是最重要的字段,标识WB协议的一个字段
     

// 响应头
     HTTP/1.1 101 Web Socket Protocol Handshake
     WebSocket-Location: ws://localhost:12345/websocket/test.php
     Connection: Upgrade
     Upgrade: websocket
     Sec-WebSocket-Accept: zUyzbJdkVJjhhu8KiAUCDmHtY/o= 
     WebSocket-Origin: http://localhost
     
    // Sec-WebSocket-Accept: 叫“梦幻字符串”,和上面那个梦幻字符串做用同样。不一样的是,这个字符串是要让客户端辨认的,客户端拿到后自动解码。而且辨认是否是一个WB请求。而后进行相应的操做。这个字段也是重中之重,不可随便修改的。加密规则,依然是有规则的
复制代码

WebSocket客户端

在客户端,没有必要为WebSockets使用JavaScript库。实现WebSocketsWeb 浏览器将经过WebSockets对象公开全部必需的客户端功能(主要指支持HTML5的浏览器)git

客户端 API

如下 API 用于建立WebSocket对象。github

var Socket = new WebSocket(url, [protocol] );
复制代码
  • 以上代码中的第一个参数url, 指定链接的URL
  • 第二个参数protocol是可选的,指定了可接受的子协议

WebSocket属性

如下是WebSocket对象的属性。假定咱们使用了以上代码建立了Socket对象web

  • Socket.readyState: 只读属性readyState表示链接状态, 能够是如下值
    • 0 : 表示链接还没有创建
    • 1 : 表示链接已创建,能够进行通讯
    • 2 : 表示链接正在进行关闭
    • 3 : 表示链接已经关闭或者链接不能打开。
  • Socket.bufferedAmount: 只读属性bufferedAmount
    • 表示已被send() 放入正在队列中等待传输,可是尚未发出的UTF-8文本字节数

WebSocket事件

如下是WebSocket对象的相关事件。假定咱们使用了以上代码建立了Socket 对象:express

事件 事件处理程序 描述
open Socket.onopen 链接创建时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通讯发生错误时触发
close Socket.onclose 链接关闭时触发

WebSocket方法

如下是WebSocket对象的相关方法。假定咱们使用了以上代码建立了Socket对象:npm

方法 描述
Socket.send() 使用链接发送数据
Socket.close() 关闭链接

示例

// 客户端
var socket = new WebSocket("ws://localhost:9090")

// 创建 web socket 链接成功触发事件
socket.onopen = function () {
    // 使用send发送数据
    socket.send("发送数据")
    console.log(socket.bufferedAmount)
    alert('数据发送中')
}

// 接受服务端数据是触发事件
socket.onmessage = function (evt) {
    var received_msg = evt.data
    alert('数据已经接受..')
}

// 断开 websocket 链接成功触发事件
socket.onclose = function () {
    alert('连接已经关闭')
    console.log(socket.readyState)
}

复制代码

WebSocket服务端

WebSocket在服务端的实现很是丰富。Node.jsJavaC++Python 等多种语言都有本身的解决方案, 其中Node.js经常使用的有如下三种json

下面就着重研究一下Socket.IO吧, 由于别的我也不会, 哈哈哈哈......swift

Socket.IO

  • Socket.IO是一个库,能够在浏览器和服务器之间实现实时,双向和基于事件的通讯
  • Socket.IO是一个彻底由JavaScript实现、基于Node.js、支持WebSocket的协议用于实时通讯、跨平台的开源框架
  • Socket.IO包括了客户端(iOS,Android)和服务器端(Node.js)的代码,能够很好的实现iOS即时通信技术
  • Socket.IO支持及时、双向、基于事件的交流,可在不一样平台、浏览器、设备上工做,可靠性和速度稳定
  • Socket.IO其实是WebSocket的父集,Socket.io封装了WebSocket和轮询等方法,会根据状况选择方法来进行通信
  • 典型的应用场景如:
    • 实时分析:将数据推送到客户端,客户端表现为实时计数器、图表、日志客户
    • 实时通信:聊天应用
    • 二进制流传输:socket.io支持任何形式的二进制文件传输,例如图片、视频、音频等
    • 文档合并:容许多个用户同时编辑一个文档,并可以看到每一个用户作出的修改

Socket.IO服务端

  • Socket.IO实质是一个库, 因此在使用以前必须先导入Socket.IO
  • Node.js导入库和iOS导入第三方库性质同样, 只不过iOS使用的是pods管理, Node.js使用npm

导入Socket.IO

// 1. 进入当当前文件夹
cd ...

// 2. 建立package.json文件
npm init

/// 3. 导入库
npm install socket.io --sava
npm install express --sava
复制代码

建立socket

  • socket本质仍是http协议,因此须要绑定http服务器,才能启动socket服务.
  • 并且须要经过web服务器监听端口,socket不能监听端口,有人访问端口才能创建链接,因此先建立web服务器
// 引入http模块
var http = require('http')

// 面向express框架开发,加载express框架,方便处理get,post请求
var express = require('express')

// 建立web服务器
var server = http.Server(express)

// 引入socket.io模块
var socketio = require('socket.io')

// 建立爱你socket服务器
var serverSocket = socketio(server)


server.listen(9090)
console.log('监听9090')
复制代码

创建socket链接

  • 服务器不须要主动创建链接,创建链接是客户端的事情,服务器只须要监听链接
  • 客户端主动链接会发送connection事件,服务端只须要监听connection事件有没有发送,就知道客户端有没有主动链接服务器
  • Socket.IO本质是经过发送和接受事件触发服务器和客户端之间的通信,任何能被编辑成JSON或二进制的对象均可以传递
  • socket.on: 监听事件,这个方法会有两个参数,第一个参数是事件名称,第二个参数是监听事件的回调函数,监听到连接就会执行这个回调函数
  • 监听connection,回调函数会传入一个链接好的socket,这个socket就是客户端的socket
  • socket链接原理,就是客户端和服务端经过socket链接,服务器有socket,客户端也有
// 监听客户端有没有链接成功,若是链接成功,服务端会发送connection事件,通知客户端链接成功
// serverSocket: 服务端, clientSocket: 客户端
serverSocket.on('connection', function (clientSocket) {
    // 创建socket链接成功
    console.log('创建链接成功')

    console.log(clientSocket)
})
复制代码

Socket.IO客户端

建立socket对象

建立SocketIOClient对象, 两种建立方式api

// 第一种, SocketIOClientConfiguration: 可选参数
public init(socketURL: URL, config: SocketIOClientConfiguration = [])

// 第二种, 底层仍是使用的第一种方式建立
public convenience init(socketURL: URL, config: [String: Any]?) {
        self.init(socketURL: socketURL, config: config?.toSocketConfiguration() ?? [])
}
复制代码
  • SocketIOClientConfiguration: 是一个数组, 等同于[SocketIOClientOption]
  • SocketIOClientOption的全部取值以下
public enum SocketIOClientOption : ClientOption {
    /// 使用压缩的方式进行传输
    case compress
    /// 经过字典内容链接
    case connectParams([String: Any])
    /// NSHTTPCookies的数组, 在握手过程当中传递, Default is nil.
    case cookies([HTTPCookie])
    /// 添加自定义请求头初始化来请求, 默认为nil
    case extraHeaders([String: String])
    /// 将为每一个链接建立一个新的connect, 若是你在从新链接有bug时使用.
    case forceNew(Bool)
    /// 传输是否使用HTTP长轮询, 默认false
    case forcePolling(Bool)
    /// 是否使用 WebSockets. Default is `false`
    case forceWebsockets(Bool)
    /// 调度handle的运行队列, 默认在主队列
    case handleQueue(DispatchQueue)
    /// 是否打印调试信息. Default is false
    case log(Bool)
    /// 可自定义SocketLogger调试日志
    case logger(SocketLogger)
    /// 自定义服务器使用的路径.
    case path(String)
    /// 连接失败时, 是否从新连接, Default is `true`
    case reconnects(Bool)
    /// 从新链接多少次. Default is `-1` (无限次)
    case reconnectAttempts(Int)
    /// 等待重连时间. Default is `10`
    case reconnectWait(Int)
    /// 是否使用安全传输, Default is false
    case secure(Bool)
    /// 设置容许那些证书有效
    case security(SSLSecurity)
    /// 自签名只能用于开发模式
    case selfSigned(Bool)
    /// NSURLSessionDelegate 底层引擎设置. 若是你须要处理自签名证书. Default is nil.
    case sessionDelegate(URLSessionDelegate)
}
复制代码

建立SocketIOClient

// 注意协议:ws开头
guard let url = URL(string: "ws://localhost:9090") else { return }
let manager = SocketManager(socketURL: url, config: [.log(true), .compress])
// SocketIOClient
let socket = manager.defaultSocket
复制代码

监听链接

  • 建立好socket对象,而后链接用connect方法
  • 由于socket须要进行3次握手,不可能立刻建议链接,须要监听是否链接成功的回调,使用on方法
  • ON方法两个参数
    • 参数一: 监听的事件名称,参数二:监听事件回调函数,会自动调用
    • 回调函数也有两个参数(参数一:服务器传递的数据 参数二:确认请求数据ACK)
    • TCP/IP协议中,若是接收方成功的接收到数据,那么会回复一个ACK数据- ACK只是一个标记,标记是否成功传输数据
// 回调闭包
public typealias NormalCallback = ([Any], SocketAckEmitter) -> ()

// on方法
@discardableResult
open func on(_ event: String, callback: @escaping NormalCallback) -> UUID

// SocketClientEvent: 接受枚举类型的on方法
@discardableResult
open func on(clientEvent event: SocketClientEvent, callback: @escaping NormalCallback) -> UUID {
    // 这里调用的是上面的on方法
    return on(event.rawValue, callback: callback)
}
复制代码

完整代码

guard let url = URL(string: "ws://localhost:9090") else { return }

let manager = SocketManager(socketURL: url, config: [.log(true), .compress])
let socket = manager.defaultSocket

// 监听连接成功
socket.on(clientEvent: .connect) { (data, ack) in
    print("连接成功")
    print(data)
    print(ack)
}
        
socket.connect()
复制代码

SocketIO事件

SocketIO经过事件连接服务器和传递数据

客户端监听事件

// 监听连接成功
socket.on(clientEvent: .connect) { (data, ack) in
    print("连接成功")
    print(data)
    print(ack)
}
复制代码

客户端发送事件

只有链接成功以后,才能发送事件

// 创建一个链接到服务器. 链接成功会触发 "connect"事件
open func connect()

// 链接到服务器. 若是链接超时,会调用handle
open func connect(timeoutAfter: Double, withHandler handler: (() -> ())?)

// 重开一个断开链接的socket
open func disconnect()

// 向服务器发送事件, 参数一: 事件的名称,参数二: 传输的数据组
open func emit(_ event: String, with items: [Any])
复制代码

服务器监听事件

  • 监听客户端事件,须要嵌套在链接好的connect回调函数中
  • 必须使用回调函数的socket参数,如function(s)中的s,监听事件,所以这是客户端的socket,确定监听客户端发来的事件
  • 服务器监听链接的回调函数的参数能够添加多个,具体看客户端传递数据数组有几个,每一个参数都是与客户段一一对应,第一个参数对应客户端数组第0个数据
// 监听socket链接
socket.on('connection',function(s){

    console.log('监听到客户端链接');

    // data:客户端数组第0个元素
    // data1:客户端数组第1个元素
    s.on('chat',function(data,data1){

        console.log('监听到chat事件');

        console.log(data,data1);
        
    });
});
复制代码

服务器发送事件

这里的socket必定要用服务器端的socket

// 给当前客户端发送数据,其余客户端收不到.
socket.emit('chat', '服务器' + data)

// 发给全部客户端,不包含当前客户端
socket.emit.broadcast.emit('chat', '发给全部客户端,不包含当前客户端' + data)

// 发给全部客户端,包含当前客户端
socket.emit.sockets.emit('chat', '发给全部客户端,包含当前客户端' + data)
复制代码

SocketIO分组

  • 每个客户端和服务器只会保持一个socket连接, 那么怎么吧每一条信息推送到对应的聊天室, 针对多个聊天室的问题有如何解决
  • 给每一个聊天室都分组, 服务器就能够给指定的组进行数据的推送, 就不会影响到其余的聊天室

如何分组

  • socket.io提供rooms和namespace的API
  • rooms的API就能够实现多房间聊天了,总结出来无外乎就是:join/leave roomsay to room
  • 这里的socket是客户端的socket,也就是链接成功,传递过来的socket
// join和leave
io.on('connection', function(socket){
  socket.join('some room');
  // socket.leave('some room');
});
 
// say to room
io.to('some room').emit('some event'):
io.in('some room').emit('some event'):
复制代码

分组的原理

  • 只要客户端socket调用join,服务器就会把客户端socket和分组的名称绑定起来
  • 到时候就能够根据分组的名称找到对应客户端的socket,就能给指定的客户端推送信息
  • 一个客户端socket只能添加到一组,离开的时候,要记得移除

欢迎您扫一扫下面的微信公众号,订阅个人博客!

微信公众号
相关文章
相关标签/搜索