[備忘録]SwiftでWebSocketを試してみる


SwiftでWebSocketを試してみた。
サーバーは楽なのでSinatraを使う。。。

・Sinatra側WebSocketサーバー
(1) sinatraでwebsocket使えるようgemをインストール

sudo gem install sinatra-websocket

(2) test_app1.rb

# coding: utf-8                                                                                                                
require 'sinatra'
require 'sinatra/reloader'
require 'sinatra-websocket'
require 'json'

# 接続ポート                                                                                                                   
set :port, 1234
set :server, 'thin' #, group: :development                                                                                     
set :sockets, []

# 標準出力ログ出力                                                                                                             
$stdout.sync = true

get '/' do
  erb :fuga
end

get '/pingpong' do
  if request.websocket? then
    puts "request received...:[#{request.inspect}]"
    request.websocket do |ws|
      ws.onopen do
        #puts "ws on open..:[#{ws.inspect}]"
        settings.sockets << ws
      end
      ws.onmessage do |msg|
         #puts "ws on receive msg.."
         settings.sockets.each do |s|
          s.send("gkgk pong:#{msg}")
         end
      end
      ws.onclose do
         settings.sockets.delete(ws)
      end
    end
  end
end

/pingpongというURLでWebSocketのリクエストを受ける
クライアント側がメッセージを投げたら、"gkgk pong:#{投げたメッセージ}"を返却する感じ。

(3) views/fuga.erb

<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>hoge</title>

</head>
   <body>
    <form id="form">
      <input type="text" id="send_msg">
      <input type="submit">
    </form>
    <ul id="msgs"></ul>
<script type="text/javascript">
  (function (){
  window.onload = function(){
  var msgbox = document.getElementById("msgs");
  var form = document.getElementById("form");
  var send_msg = document.getElementById("send_msg");

  var tttt = 'ws://' + window.location.host + "/pingpong";
  console.log(tttt);

  var ws = new WebSocket('ws://' + window.location.host + "/pingpong");

  ws.onopen = function() {
  console.log("connection opened");
  }
  ws.onclose = function() {
  console.log("connection closed");
  }
  ws.onmessage = function(m) {
  var li = document.createElement("li");
  li.textContent = m.data;
  msgbox.insertBefore(li, msgbox.firstChild);
  }

  send_msg.onclick = function(){
  send_msg.value = "";
  }

  form.onsubmit = function(){
  ws.send(send_msg.value);
  send_msg.value = "";
  return false;
  }
  }
  })();
</script>

ブラウザでも確認出来るよーに、インデックスページを用意。。。

(4) サーバー起動

ruby ./test_app1.rb 
== Sinatra (v2.0.0) has taken the stage on 1234 for development with backup from Thin
Thin web server (v1.7.0 codename Dunder Mifflin)
Maximum connections set to 1024
Listening on 0.0.0.0:1234, CTRL+C to stop

ポート:1234で起動する。

・iOS側WebSocketクライアント

画面UIは以下のよーなイメージ。

イベントのログを表示するTableViewを画面上部、
真ん中に、送信するメッセージを入力するテキストボックス、
その下に、サーバーとの接続ボタンを配置した。

(1) シングルビューのSwiftのプロジェクトを作成し、cocoapodsで、Swift用のWebSocketのライブラリをインストール

platform :ios, '9.0'
use_frameworks!

target 'WebSockTest1' do
  pod 'Reachability'
  pod 'XCGLogger'
  pod 'SwiftWebSocket'
end

プロジェクト名は、「WebSockTest1」で作成している。
使用したライブラリは、「SwiftWebSocket」。

(2) ViewControllerのソースを以下のように。。。

import UIKit

import SwiftWebSocket

class ViewController: UIViewController,
    UITableViewDataSource,
    UITableViewDelegate,
    UITextFieldDelegate {


    static let HOGE_URL = "ws://localhost:1234/pingpong"


    @IBOutlet weak var tblChat: UITableView!

    @IBOutlet weak var opeContainer: UIView!

    @IBOutlet weak var txtMessage: UITextField!

    @IBOutlet weak var btnConnect: UIButton!

    @IBOutlet weak var opeContainerHeight: NSLayoutConstraint!

    var ws: WebSocket!
    var flgWsInit = false

    var eventLogs = [String]()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupWs()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func tapBtnConnect(_ sender: Any) {

        if ws == nil || ws.readyState != .open {
            if ws == nil {
                setupWs()
            }
            ws.open(ViewController.HOGE_URL)
            btnConnect.isEnabled = false
        }
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        let msg = textField.text!
        if msg == "" {
            return true
        }
        if ws.readyState == .open {
            ws.send(msg)
            self.addWsEvent(ev: "send: \(msg)")
        }
        textField.text = ""
        return true
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 0.0
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 80.0
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        var cell: EventCell?

        cell = tableView.dequeueReusableCell(withIdentifier: "CELL_EV",
                                             for: indexPath) as? EventCell

        if cell == nil {

            cell = EventCell.instance()
        }

        cell!.txtEvent.text = eventLogs[indexPath.item]

        return cell!
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return eventLogs.count
    }


    private func setupUI() {

        txtMessage.text = ""
        txtMessage.clearButtonMode = .always
        txtMessage.returnKeyType = .go
        txtMessage.autocapitalizationType = .none
        txtMessage.spellCheckingType = .no
        txtMessage.delegate = self
        if txtMessage.canBecomeFirstResponder {
            txtMessage.becomeFirstResponder()
        }

        tblChat.layer.borderWidth = 1.0
        tblChat.layer.borderColor = UIColor.lightGray.cgColor
        tblChat.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        tblChat.register(
            UINib(nibName: "EventCell", bundle: nil),
            forCellReuseIdentifier: "CELL_EV")
        tblChat.dataSource = self
        tblChat.delegate = self

    }


    func addWsEvent(ev: String) {

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {[unowned self]  in

            self.tblChat.beginUpdates()
            self.eventLogs.insert(ev, at: 0)
            let indexPath = IndexPath(row: 0, section: 0)
            self.tblChat.insertRows(at: [indexPath], with: .automatic)
            self.tblChat.endUpdates()

            for idx in self.tblChat.indexPathsForVisibleRows! {
                if idx.item != 0 {
                    self.tblChat.cellForRow(at: idx)?.setSelected(false, animated: false)
                }
            }
            self.tblChat.cellForRow(at: indexPath)?.setSelected(true, animated: false)

            if self.txtMessage.canBecomeFirstResponder {
                self.txtMessage.becomeFirstResponder()
            }
        }
    }


    private func setStateBtnConn(isEnable: Bool) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {[unowned self]  in
            self.btnConnect.isEnabled = isEnable
        }
    }


    private func setupWs() {

        ws = WebSocket()
//        ws = WebSocket(ViewController.HOGE_URL)
        if !flgWsInit {
            ws.event.open = {[unowned self] in
                log.debug("opened:\(ViewController.HOGE_URL)")
                self.addWsEvent(ev: "opened:\(ViewController.HOGE_URL)")
                self.setStateBtnConn(isEnable: false)
            }
            ws.event.close = {[unowned self] code, reason, clean in
                log.debug("close")
                self.addWsEvent(ev: "close.")
                self.setStateBtnConn(isEnable: true)
            }
            ws.event.error = {[unowned self] error in
                log.debug("error \(error)")
                self.addWsEvent(ev: "error \(error)")
            }
            ws.event.message = {[unowned self] message in
                if let text = message as? String {
                    log.debug("recv: \(text)")
                    self.addWsEvent(ev: "recv: \(text)")
                    if self.ws.readyState != .open {
                        self.setStateBtnConn(isEnable: false)
                    }
                }
            }
            flgWsInit = true
        }
    }

}

iOSアプリ側の仕様は、

・ConnectボタンでWebSocketサーバーに接続する。
・テキストボックスにメッセージを入力し、エンターキーを入力すると、サーバーにメッセージを送信する。
・サーバーからのレスポンスは、UITableViewのセルを追加して表示する。
(UIスレッドじゃないはずなので、表示はUIスレッドに遅延で更新するようにした。(DispatchQueue.main.asyncAfter))

という感じ。

ローカルでテストするので、Info.plistにセキュアでない通信を許可するよう(ATS)設定しておく。

・sinatraを起動した状態で、エミュレーターで実行してみる。

(1)アプリ起動

(2)Connectボタンをタップ

接続したログが、テーブルビューに追加される。

(3)テキストボックスに「HOGE」を入力し、エンターキーを押す。

「HOGE」を送信したログが追加され、続けて、
「gkgk pong:HOGE」がサーバーから返答されてるログを表示。

(4)インデックスページを立ち上げ、エミュレーターをそのままにし、
インデックスページからメッセージを送信してみる。

(5)「fuga」と入力し、送信ボタンをタップ。

エミュレーターの状態を見てみる。

「fuga」を受信している。

これは、
Sinatra上で接続をArrayにキャッシュしていて、いま接続中のコネクションに全てに対して、メッセージを送信しているから。

という感じで、SinatraとCocoaPodsのライブラリで簡単に試すことが出来た。。。