【如何快速的开发一个完整的iOS直播app】(搭建Socket即时通讯服务器)

前言

在看这篇之前,如果您还不了解直播原理,请查看这篇文章如何快速的开发一个完整的iOS直播app(原理篇)

在直播中,聊天和发礼物,需要用到及时通讯技术,市面上的App大多数采用的都是第三方SDK,融云,环信等,但是本例子采用websocket搭建及时通讯服务器。

如果喜欢我的文章,可以关注我微博:袁峥Seemygo

即时通讯

即时通讯(Instant messaging,简称IM)是一个终端服务,允许两人或多人使用网路即时的传递文字讯息、档案、语音与视频交流

即时通讯技术原理(了解Socket)

  • Socket介绍: 套接字或者插座,用于描述IP地址和端口号,是一种网络的通信机制。
  • Socket作用: 网络通信底层都是通过socket建立连接的,因为它包含IP和端口,只要有这两个就能准确找到一台主机上的某个应用。
  • IM通信原理(T):
    • 客户端A与客户端B如何产生通信?客户端A不能直接和客户端B,因为两者相距太远。
    • 这时就需要通过IM服务器,让两者产生通信.
    • 客户端A通过socket与IM服务器产生连接,客户端B也通过socket与IM服务器产生连接
    • A先把信息发送给IM应用服务器,并且指定发送给B,服务器根据A信息中描述的接收者将它转发给B,同样B到A也是这样。
    • 通讯问题: 服务器是不能主动连接客户端的,只能客户端主动连接服务器
    • 那么当服务器要推信息给客户端B,但是客户端B这时候没有与服务器产生连接,就推送不了.
    • 这样就延迟,不即时了。

即时通讯连接原理

  • 即时通讯都是长连接,基本上都是HTTP1.1协议,设置Connection为keep-alive即可实现长连接,而HTTP1.1默认是长连接,也就是默认Connection的值就是keep-alive。
  • HTTP分为长连接和短连接,其实本质上是TCP连接,HTTP协议是应用层的协议,而TCP才是真正的传输层协议,只有负责传输的这一层才需要建立连接。
  • 就拿网上购物来说,HTTP协议指的那个快递单,你寄件的时候填的单子就像是发了一个HTTP请求,等货物运到地方了,快递员会根据你发的请求把货物送给相应的收货人。而TCP协议就是中间运货的那个大货车,也可能是火车或者飞机,但不管是什么,它是负责运输的,因此必须要有路,不管是地上还是天上。那么这个路就是所谓的TCP连接
  • Http连接:只要服务端给了响应,本次HTTP连接就结束,本质不存在没有长连接。
  • htpp长连接指的是:长连接是为了复用,长连接是指的TCP连接,也就是说复用的是TCP连接,长连接情况下,多个HTTP请求可以复用同一个TCP连接,这就节省了很多TCP连接建立和断开的消耗,要不然每个Http请求都产生一个TCP连接,浪费很多资源

即时通讯(数据即时传递原理)

  • 即时通讯核心是`即时``,那怎么达到即时?
    • 目前实现即时通讯的有四种方式(短轮询、长轮询、SSE、Websocket)
    • 短轮询: 每隔一小段时间就发送一个请求到服务器,服务器返回最新数据,然后客户端根据获得的数据来更新界面,这样就间接实现了即时通信。优点是简单,缺点是对服务器压力较大,浪费带宽流量(通常情况下数据都是没有发生改变的)。
    • 短轮询: 主要是客户端人员写代码,服务器人员比较简单,适于小型应用
    • 长轮询: 客户端发送一个请求到服务器,服务器查看客户端请求的数据(服务器中数据)是否发生了变化(是否有最新数据),如果发生变化则立即响应返回,否则保持这个连接并定期检查最新数据,直到发生了数据更新或连接超时。同时客户端连接一旦断开,则再次发出请求,这样在相同时间内大大减少了客户端请求服务器的次数.
    • 长轮询底层实现:在服务器的程序中加入一个死循环,在循环中监测数据的变动。当发现新数据时,立即将其输出给浏览器并断开连接,浏览器在收到数据后,再次发起请求以进入下一个周期
    • 长轮询弊端:服务器长时间连接会消耗资源,返回数据顺序无保证,难于管理维护
    • 长轮询处理:不能一直持续下去,应该设定一个最长时限,可以通过心跳包的方式,设置多少秒没有接到心跳包,就关闭当前连接。
    • 心跳包:就是在客户端和服务器间定时通知对方自己状态的一个自己定义的命令字,按照一定的时间间隔发送,类似于心跳,所以叫做心跳包
    • SSE(Server-sent Events服务器推送事件):为了解决浏览器只能够单向传输数据到服务端,HTML5提供了一种新的技术叫做服务器推送事件SSE,SSE技术提供的是从服务器单向推送数据给浏览器的功能,加上配合浏览器主动Http请求,两者结合起来,实际上就实现了客户端和服务器的双向通信.
    • WebSocket:上面的这些解决方案中,都是利用浏览器单向请求服务器或者服务器单向推送数据到浏览器,而在HTML5中,为了加强web的功能,提供了websocket技术,它不仅是一种web通信方式,也是一种应用层协议。它提供了浏览器和服务器之间原生的全双工跨域通信,通过浏览器和服务器之间建立websocket连接,在同一时刻能够实现客户端到服务器和服务器到客户端的数据发送.

WebSocket

  •  什么是websocket?WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 客户端 都能主动的向对方发送或接收数据。
    
  • websocket原理?WebSocket是基于Http协议的,或者说借用了Http协议来完成一部分握手(连接),在握手(连接)阶段与Http是相同的,只不过Http不能服务器给客户端推送,而websocket可以。
  • websocket协议头:ws
  •  服务器根据协议头判断是Http,还是websocket、
    
  • Websocket协议解析:
    • 请求头

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

           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= //这个字段,叫“梦幻字符串”,和上面那个梦幻字符串作用一样。不同的是,这个字符串是要让客户端辨认的,客户端拿到后自动解码。并且辨认是不是一个WB请求。然后进行相应的操作。这个字段也是重中之重,不可随便修改的。加密规则,依然是有规则的,可以去百度一下。
           WebSocket-Origin: http://localhost
      
    • Sec-WebSocket-Key:其值采用base64编码的随机16字节长的字符序列

    • Sec-WebSocket-Accept如何生成

Socket.IO简介

  • 为什么要使用Socket.IO?WebSocket的功能是很强大的,使用起来也灵活,可以适用于不同的场景。不过WebSocket技术也比较复杂,需要加密解密,包装协议,自己实现3次握手,还需要对数据流进行加密解密处理,服务器端和浏览器端的实现都不同于一般的Web应用,因此自己实现很麻烦,可以使用Socket.IO框架。

  • Socket.IO:是一个完全由JavaScript实现、基于Node.js、支持WebSocket的协议用于实时通信、跨平台的开源框架。

  • Socket.IO:它包括了客户端(iOS,Android)和服务器端(Node.js)的代码,可以很好的实现iOS即使通讯技术。

  • Socket.IO框架地址

Socket.IO教程

Socket.IO建立连接 服务器代码

  • 1.如何导入Socket.IO?

    • 和导入express框架一样,使用package
    • 给package文件添加依赖
        "dependencies": {
                        "express": "^4.14.0",
                        "socket.io": "^1.4.8"
                        }
    
  • 2.如何创建socket

    • socket本质还是http协议,所以需要绑定http服务器,才能启动socket服务.

    • 而且需要通过web服务器监听端口,socket不能监听端口,有人访问端口才能建立连接,所以先创建web服务器

*   1.面向express框架开发,加载express框架,方便处理get,post请求

*   2.因为socket依赖http,创建http服务器,使用http模块.

*   3.可以通过express创建http服务器http.server(express)

*   4.通过http服务器创建socket

*   5.监听http服务器

```
    // 引入express
var http = require('http');

var express = require('express');

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

// 引入socker
var socketIO = require('socket.io');

// 需要传入服务器,socket基于http
var socket = socketIO(server);

// 监听web服务器
server.listen(8080);
```
  • 3.如何建立socket连接(服务器不需要主动建立连接,建立连接是客户端的事情,服务器只需要监听连接)

    • 客户端主动连接会发送connection事件,只需要监听connection事件有没有发送,就知道客户端有没有主动连接服务器
    • Socket.IO本质是通过发送和接受事件触发服务器和客户端之间的通讯,任何能被编辑成JSON或二进制的对象都可以传递。
    • 监听事件,用socket.on,这个方法会有两个参数,第一个参数是事件名称,第二个参数是监听事件的回调函数,监听到就会执行这个回调函数
    • 监听connection,回调函数会传入一个连接好的socket,这个socket就是客户端的socket
    • socket连接原理,就是客户端和服务端通过socket连接,服务器有socket,客户端也有
        // 监听socket连接
        // function参数必填socket
        socket.on('connection',function(clientSocket){
            console.log('建立连接',clientSocket);
        });
    
    
    • 书写客户端代码,验证是否能建立连接

Socket.IO建立连接 客户端代码

  • 1.下载Socket.IO-Client-Swift

    • Socket.IO只有swift,如果需要用OC代码,需要swift和OC混编
    • 还有如果代码是OC,并且使用cocoapods,就不要使用cocoapods导入swift代码,会有问题.
  • 2.下载完了,直接把Source文件夹拖入到自己工程中.

    • 会报错,说当前swift版本过时,需要更新。点击Xcode顶部Edit => Convert => TO Current Swift Syntas 就好了。
  • 3.OC和Swift混编,Swift代码怎么在OC中使用,直接导入"工程文件名-Swift.h"就可以使用,这个文件Xcode会自动帮我们生成,无序手动自己生成.

    #import "客户端-Swift.h"
  • 4.注意工程文件名不能带有-这个符号,而且有时候会延迟,并不是马上导入"工程文件名-Swift.h"就好.

  • 5.创建socket对象,然后连接用connect方法,socket对象需要强引用

    • 注意协议:ws开头

    • 创建socket对象,需要传入字典,字典配置如下。

          所有关于SocketIOClientOption的设置.如果是ObjC,转换名字lowerCamelCase.
          case ConnectParams([String: AnyObject]) // 通过字典内容连接
          case Cookies([NSHTTPCookie]) // An array of NSHTTPCookies. Passed during the handshake. Default is nil.
          case DoubleEncodeUTF8(Bool) // Whether or not to double encode utf8. If using the node based server this should be true. Default is true.
          case ExtraHeaders([String: String]) // 添加自定义请求头初始化来请求, 默认为nil
          case ForcePolling(Bool) // 是否使用 xhr-polling. Default is `false`
          case ForceNew(Bool) // 将为每个连接创建一个新的connect, 如果你在重新连接时有bug时使用.
          case ForceWebsockets(Bool) // 是否使用 WebSockets. Default is `false`
          case HandleQueue(dispatch_queue_t) // 调度handle的运行队列. Default is the main queue.
          case Log(Bool) // 是否打印调试信息. Default is false.
          case Logger(SocketLogger) // 可自定义SocketLogger调试日志.默认是系统的.
          case Nsp(String) // 如果使用命名空间连接. Must begin with /. Default is `/`
          case Path(String) // 如果服务器使用一个自定义路径. 例如: `"/swift/"`. Default is `""`
          case Reconnects(Bool) // 是否重新连接服务器失败. Default is `true`
          case ReconnectAttempts(Int) // 重新连接多少次. Default is `-1` (无限次)
          case ReconnectWait(Int) // 等待重连时间. Default is `10`
          case SessionDelegate(NSURLSessionDelegate) // NSURLSessionDelegate 底层引擎设置. 如果你需要处理自签名证书. Default is nil.
          case Secure(Bool) // 如果连接要使用TLS. Default is false.
          case SelfSigned(Bool) //  WebSocket.selfSignedSSL设置 (Don't do this, iOS will yell at you)
          case VoipEnabled(Bool) // 如果你的客户端使用VoIP服务,只有用这个选项,Default is false  
      
  • 6.因为需要进行3次握手,不可能马上建议连接,需要监听是否连接成功的回调,使用on方法

  • 7.ON方法两个参数(第一个参数,监听的事件名称,第二个参数:监听事件回调函数,会自动调用)

    • 回调函数也有两个参数(第一个参数:服务器传递的数据 第二个参数:确认请求数据)
    • 在TCP/IP协议中,如果接收方成功的接收到数据,那么会回复一个ACK数据。
    •  ACK只是一个标记,标记是否成功传输数据。
      
    NSURL *url = [NSURL URLWithString:@"ws://192.168.0.100:8080"];
    
    SocketIOClient *socket = [[SocketIOClient alloc] initWithSocketURL:url config:@{@"log": @YES, @"forcePolling": @YES}];
    _socket = socket;
    
    [socket connect];
    
    // 监听连接成功
    [socket on:@"connect" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ask) {
        
        NSLog(@"确定与服务器连接");
        
        NSLog(@"%@ %@",data,ask);
        
    }];

SocketIO发送事件,通过事件传递数据

SocketIO 客户端发送事件代码

  • 注意:只有连接成功之后,才能发送事件
  • 向服务器发送事件(emit:第一参数事件的名称,第二个参数传输的数据,是一个数组)
    [socket emit:@"chat" with:@[@"你好"]];

SocketIO 服务器监听事件代码

  • 监听客户端事件,需要嵌套在连接好的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);
        
    });
    
});

SocketIO 服务器发送事件代码

  • 这里的socket一定要用服务器端的socket
  • 给当前客户端发送数据,其他客户端收不到.
    socket.emit('chat','服务器'+data);
  • 发给所有客户端,不包含当前客户端
    socket.emit.broadcast.emit('chat','发给所有客户端,不包含当前客户端'+data);
  • 发给所有客户端,包含当前客户端
    socket.emit.sockets.emit('chat','发给所有客户端,包含当前客户端'+data);

SocketIO 客户端监听事件代码

    [socket on:@"chat" callback:^(NSArray * _Nonnull data, SocketAckEmitter * _Nonnull ask) {
        NSLog(@"%@",data[0]);
    }];

SocketIO分组

  • 开发中什么场景需要使用SocketIO分组?(T)
  • 一个客户端和服务器只会保持一个socket连接,比如直播App中会开很多主播房间,每个房间都有自己的聊天室,那怎么把信息推送到对应的房间,比如A用户要给A主播间发送信息,怎么推送过去,通过服务器只能给当前客户端推送,那一推,当前客户端所有直播间都有A用户的信息。
  • 怎么解决多个直播聊天室问题?
  • 给每个主播的房间都分组,服务器就可以给指定组推送数据,就不会影响到其他直播间
  • SocketIO如何分组?
    • 服务器代码: socket.join(),()里面放分组名称,与之对应的 socket.leave()
    • 注意这里的socket是客户端的socket,也就是连接成功,传递过来的socket
  • socket分组的原理,只要客户端socket调用join,服务器就会把客户端socket和分组的名称绑定起来,到时候就可以根据分组的名称找到对应客户端的socket,就能给指定的客户端推送信息.
  • 注意:一个客户端socket只能添加到一组,离开的时候,要记得移除.
  • 客户端可以这样测试,搞两台电脑/两台手机在同一个局域网内,运行就有两个客户端,分别加入不同组.
  • 服务器只给一个客户端socket发送信息,另外一个客户端收不到
  • 服务器代码
// 监听socket连接
socket.on('connection',function(s){

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

    s.on('createRoom',function(roomName){

        s.join(roomName);
        rooms.push(roomName);
        console.log('创建房间'+ roomName);
    });

    s.on('chatRoom',function(data){
        console.log(rooms[0] + '说话');
        socket.to(rooms[0]).emit('chat','房间1的数据');
    });

});

推荐阅读更多精彩内容