关于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") } }