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