LoginSignup
2
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-06-05

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は以下のよーなイメージ。
UIimage.png

イベントのログを表示する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)設定しておく。

plist.png

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

(1)アプリ起動
UIimage.png

(2)Connectボタンをタップ
UIimage_tap_conn.png

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

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

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

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

sinatra_index.png

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

sinatra_send_fuga.png

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

「fuga」を受信している。

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

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

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3