iOS开发Swift

swift 使用websocket协议和服务器通信

关于websocket协议就不多做介绍了,websocket是一种长连接协议,可以实现从客户端到服务器端之间,消息的实时传送。

1.首先是服务器端的代码,这里用nodejs做为服务器端代码,nodejs,websocket有很多框架,我们用websocket ws,并且发布到IBM Cloud的 Cloud Foundry上面。

https://github.com/websockets/ws

https://www.npmjs.com/package/cfenv

https://www.kevinhoyt.com/2015/09/02/websockets-on-ibm-bluemix/

  • package.json
{
  "name": "WebsocketServer",
  "version": "1.0.0",
  "description": "WebsocketServer",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "cfenv": "^1.2.3",
    "express": "^4.16.4",
    "http": "0.0.1-security",
    "ws": "^7.4.0"
  }
}
  • server.js
var cfenv = require( 'cfenv' );
var express = require( 'express' );
var http = require( 'http' );
var WebSocket = require( 'ws' );

// Environment
var environment = cfenv.getAppEnv();

// Web
var app = express();

function noop() {};

function heartbeat() {
  console.log('get pong from client');
  this.isAlive = true;
};

function sendPong() {
  console.log('get ping from client');
  this.pong(noop);
};

// Sockets
var server = http.createServer();
var wss = new WebSocket.Server( {
  server: server
} );

 wss.on('connection', function connection(ws, req) {
   ws.isAlive = true;
   // 可以通过req.headers 取得Websocker传过来的值 进行验证, 防止没有权限的人连接服务器。
   if (req.headers.apikey != apiKey) {
    ws.close();
  }
   // 可以把req.headers的某些属性值赋给ws(client)
   ws.username = req.headers.username
   ws.uuid = req.headers.uuid
   ws.role = req.headers.role
 
   // 当收到客户端的Pong时,设定该connection isAlive
   ws.on('pong', heartbeat);
   // 收到客户端的Ping时, 返回Pong
   ws.on('ping', sendPong);
   ws.on('message', function incoming(data) {
     wss.clients.forEach(function each(client) {

       // 上面ws赋的值, 这里可以取到使用。
       console.log( client.username );
       console.log( client.uuid );
       console.log( client.role );
       if (client.readyState === WebSocket.OPEN) {
         client.send(data);
       }
     });
   });
 });

// 每30秒往客户端发送Ping,然后等待客户端Pong的回答,如果超时,收不到来自客户端Pong的回答,就中断该连接,ws.terminate()。
const interval = setInterval(function ping() {
  wss.clients.forEach(function each(ws) {
    if (ws.isAlive === false) return ws.terminate();
    console.log('send ping to client');
    ws.isAlive = false;
    ws.ping(noop);
  });
}, 30000);

 wss.on('close', function close() {
  clearInterval(interval);
});

// Start
server.on( 'request', app );
server.listen( environment.port, function() {
  console.log( environment.url );
} );

2.Swift端代码,作为客户端,连接websocket服务器。Swift采用Starscream 连接webscoket服务器,可以Pod安装Starscream。

https://github.com/daltoniam/Starscream

  • Podfile文件
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'WebSocketDemo' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!
  pod 'Starscream'
  # Pods for WebSocketDemo

end
  • SceneDelegate.swift
//
//  SceneDelegate.swift
//  WebSocketDemo
//
//

import UIKit
import SwiftUI
import Starscream

class SceneDelegate: UIResponder, UIWindowSceneDelegate, WebSocketDelegate {

    var isDisconnectedFromServer = false

    func didReceive(event: WebSocketEvent, client: WebSocket) {
        switch event {
        case .connected(let headers):
            print("websocket is connected: \(headers)")
            isDisconnectedFromServer = false
            NotificationCenter.default.post(name: .connected, object: nil, userInfo: nil)
        case .disconnected(let reason, let code):
            print("websocket is disconnected: \(reason) with code: \(code)")
            NotificationCenter.default.post(name: .disconnected, object: nil, userInfo: nil)
        case .text(let string):
            print("Received text: \(string)")
            NotificationCenter.default.post(name: .text, object: string, userInfo: nil)
        case .binary(let data):
            print("Received data: \(data.count)")
        case .ping(_):
            break
        case .pong(_):
             // 收到服务器返回的Pong时, 设定接续状态
            isDisconnectedFromServer = false
            break
        case .reconnectSuggested(_):
            break
        case .error(let error):
            print("Received data: \(String(describing: error))")
        case .viabilityChanged(_):
            print("Received text: ")
        case .cancelled:
            print("Received text: )")
        }
    }

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
        // request = URLRequest(url: URL(string: "ws://localhost:1337")!)

        //request = URLRequest(url: URL(string: "https://WebSocketServer.au-syd.mybluemix.net")!)
        request = URLRequest(url: URL(string: "ws://localhost:6001")!)
        request!.timeoutInterval = 5
        // 可以用request setValue 往websocker Header里面设定值。
        request!.setValue(apiKey, forHTTPHeaderField: "apikey")
        request!.setValue("login", forHTTPHeaderField: "action")
        request!.setValue(UIDevice.current.name, forHTTPHeaderField: "username")
        request!.setValue(UIDevice.current.identifierForVendor!.uuidString, forHTTPHeaderField: "uuid")
        socket = WebSocket(request: request!)
        // 是否自动应答服务器的ping,应答回服务器Pong, 默认是true, 自动应答服务器的Ping, 返回Pong。
        socket!.respondToPingWithPong = true

        // 也可以写成
        // sceneDelegate = self
        // socket!.delegate = sceneDelegate

        socket!.delegate = self

        // 每隔20秒往服务器端发一次Ping, 看是否能收到服务器返回的Pong
        Timer.scheduledTimer(withTimeInterval: 20, repeats: true, block: { [self] (refresher) in
            if isDisconnectedFromServer == true {
                print("和服务器连接已经中断")
                // 断开以后再自动重新连接服务器。
                socket!.connect()
                NotificationCenter.default.post(name: .disconnected, object: nil, userInfo: nil)
            } else {
                isDisconnectedFromServer = true
                socket!.write(ping: Data())
            }
        })

    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
    }

}

  • ContentView.swift
//
//  ContentView.swift
//  WebSocketDemo
//
//

import SwiftUI

struct ContentView: View {
    @State var chatList: [String] = []
    @State var name: String = ""
    @State var chat: String = ""
    @State var message: String = ""
    @State var isConnectedFlag: Bool = false

    var isConnected = NotificationCenter.default.publisher(for: .connected)
    var isDisconnected = NotificationCenter.default.publisher(for: .disconnected)
    var isText = NotificationCenter.default.publisher(for: .text)

    var body: some View {
        VStack {
            
            HStack {
                TextField("メッセージ", text: self.$chat).padding(.leading, 30)
                Button(action: {
                    socket?.write(string: self.chat)
                }) {
                    Text("送信")
                }.padding(.trailing, 30).disabled(self.chat != "" ? false : true)
            }.padding(.top, 50)

            Spacer()

            ScrollView(showsIndicators: false) {
                Text("").frame(width: UIScreen.main.bounds.width - 50, height: 1)
                VStack(spacing: 10) {
                    ForEach(self.chatList, id: \.self) { message in
                        Text(message)
                    }
                }
            }.frame(width: UIScreen.main.bounds.width - 50, height: UIScreen.main.bounds.height - 180).background(Color(red: 227 / 255, green: 227 / 255, blue: 227 / 255, opacity: 1))

            Spacer()
            
            HStack {
                Button(action: {
                    if !self.isConnectedFlag {
                        socket?.connect()
                    } else {
                        socket?.disconnect()
                        self.isConnectedFlag = false
                        self.message = ""
                    }
                }) {
                    if !self.isConnectedFlag {
                        Text("サーバを接続")
                    } else {
                        Text("接続中断")
                    }

                }.padding(.leading, 30)
                Spacer()
                Text(self.message).padding(.trailing, 30)
            }.padding(.bottom, 50)
           

        }.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height, alignment: .center).onReceive(isConnected) { _ in
            self.message = "接続成功"
            self.isConnectedFlag = true
        }.onReceive(isText) { message in
            self.chatList.append(message.object as! String)
        }.onReceive(isDisconnected) { _ in
            self.message = ""
            self.isConnectedFlag = false
        }

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
  • Common.swift
//
//  Common.swift
//  WebSocketDemo
//
//

import Foundation
import Starscream

var socket: WebSocket?
var request: URLRequest?

extension Notification.Name {

    static var connected: Notification.Name {
        Notification.Name("connected")
    }
    
    static var disconnected: Notification.Name {
        Notification.Name("disconnected")
    }
    
    static var text: Notification.Name {
        Notification.Name("text")
    }
}